【问题标题】:Java Thread Safe Lazy InitializationJava 线程安全延迟初始化
【发布时间】:2013-06-30 23:41:11
【问题描述】:

我有一个网络应用程序。在 tomcat 和多个线程上运行 Servlet 调用。

我有一个 User 类、一个 Account 类和一个 1AccountContext` 类。

Accounts 可以有多个Users

每个Account 只应在内存中维护一个AccountContext 实例。

当用户通过 servlet 进行登录调用时:如果 AccountContext 存在,则返回它。否则,初始化它。

下面是我为初始化上下文而编写的代码。这段代码看起来像吗 在线程安全的同时做我想做的事?

ACCOUNT_CONTEXT_MAPConcurrentHashMap

public static AccountContext getAccountContext(Account account) {
    AccountContext accountContext = ACCOUNT_CONTEXT_MAP.get(account);
    if(accountContext == null){
        synchronized(account){
            if(ACCOUNT_CONTEXT_MAP.get(account) == null)
                accountContext = new AccountContext(account);
                //Creating the AccountContext is expensive, 
                //i'd like it if it was only done once.
                ACCOUNT_CONTEXT_MAP.put(account,accountContext);        
            }else{
                accountContext = ACCOUNT_CONTEXT_MAP.get(account);
            }
        }
    }
    return accountContext;
}

【问题讨论】:

    标签: java thread-safety lazy-initialization


    【解决方案1】:

    恕我直言,它不是线程安全的除非您保证所有线程都具有相同的帐户实例,并且没有办法让两个帐户对象代表同一个“帐户”,请考虑这种情况:两个每个线程都有一个 Account 对象,代表同一个帐户,它们都调用 getAccountContext(),第一个线程在 if(accountContext == null) 行之后立即挂起,但在开始初始化之前,然后第二个线程到达相同的时间,验证accountContext 为 null 并继续创建 AccountContext,然后再次给第一个线程 CPU 时间,因为第一个线程已经“验证”了 accountContext 为 null,它将继续创建另一个实例。

    尝试使用地图本身 (ACCOUNT_CONTEXT_MAP) 而不是每个 Account 对象进行同步。

    如果您不想在地图上同步,因为这会导致其他线程等待创建昂贵的 AccountContext,那么试试这个:

    • 创建一个新类:AccountContextBuilder:一个创建成本不高的类,用于构建一个昂贵的AccountContext。这个类将包含一个构建器方法来创建一个 AccountContext 或返回一个以前创建的。
    • 使您的地图包含 AccountContextBuilder 而不是 AccountContext 的实例。
    • 在地图上同步(无论如何你需要让它同步),这一次它不会惩罚其他线程,因为你要创建一个“便宜”的构建器对象。
    • 最后,线程使用这个构建器来访问 AccountContext,这样其他线程就不会因为其他 AccountContexts 而受到惩罚。

    【讨论】:

    • 是的,你是对的。我不能保证它们是同一个 Account 对象。但我也不想在地图本身上同步,因为我不希望不同帐户上的线程等待昂贵的 AccountContext 创建。我希望不同帐户的上下文创建并行完成,然后仅在添加到哈希映射时支付同步惩罚。
    • @user2537426 我已经编辑了我的帖子,以便为您的问题添加一个可能的解决方案,yshavit 的解决方案也是一个不错的解决方案。
    【解决方案2】:

    我会使用ConcurrentHashMap.putIfAbsent atomic 方法而不是专门为这种情况设计的同步方法。它是这样使用的:

    AccountContext accountContext = ACCOUNT_CONTEXT_MAP.get(account);
    if (accountContext == null){
        accountContext = new AccountContext(account);
        AccountContext accountContextOld = ACCOUNT_CONTEXT_MAP.putIfAbsent(account, accountContext);        
        if (accountContextOld != null) {
             accountContext = accountContextOld;
        }
    }
    return accountContext;
    

    【讨论】:

    • 我也在努力避免让多个线程启动昂贵的 AccountContext 创建。
    【解决方案3】:

    正如@morgano 指出的那样,这仅在account 是所有相同帐户的相同实例时才有效。此外,Map 必须是线程安全的——这通常意味着使用ConcurrentHashMap 或类似名称。如果您的地图不是线程安全的,那么第一行的get 就不是线程安全的——很多不好的事情都可能出错。

    你可以做的一件事就是给你的锁上条纹。创建一个包含 N 个对象的数组(字面意思是 Object 很好)。当您需要锁定synchronized 块时,从account.hashCode() % locksArray.length 获取它,然后在该对象上进行同步。这意味着您将能够并行创建许多AccountContexts,只要它们的Accounts 具有不同的hashCode() % N。平均而言,这应该会给您带来良好的性能;显然它假定Account 有一个合适的hashCode() 覆盖。

    private final Object[] locks = createLocks();
    
    private static Object[] createLocks() {
        Object[] locks = new Object[20]; // or whatever
        for (int i = 0; i < locks.length; ++i) {
            locks[i] = new Object();
        }
    }
    
    if (accountContext == null) {
        Object lock = locks[account % locks.length];
        synchronized (lock) {
            ...
        }
    }
    

    最后,这是一件小事,但在同步块中,你有:

    if(ACCOUNT_CONTEXT_MAP.get(account) == null) {
        ...
    

    我愿意:

    accountContext = ACCOUNT_CONTEXT_MAP.get(account);
    if (accountContext == null) {
        ...
    

    那么你就不需要else 块了。

    【讨论】:

    • guava中有一个非常不错的条带锁实现:com.google.common.util.concurrent.Stripeddocs.guava-libraries.googlecode.com/git/javadoc/com/google/…
    • 谢谢你的建议,看起来像我需要的。我的帐户哈希码基于我在创建帐户时分配的 UUID。根据您的建议,我从 Guava 集合中找到并检查了 Striped 类。您是否看到使用您的方法与返回 java util 并发锁的 Striped 的任何权衡? ** 在我看到 Keith 的回复之前发布...
    猜你喜欢
    • 1970-01-01
    • 2014-07-11
    • 2011-11-17
    • 2015-07-27
    • 1970-01-01
    • 1970-01-01
    • 2012-03-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多