Java线程安全延迟初始化

时间:2013-06-30 23:41:12

标签: java thread-safety lazy-initialization

我有一个网络应用程序。在tomcat上运行,并为Servlet次呼叫提供多个线程。

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

Accounts可以有多个Users

AccountContext个内存中只应保留一个Account实例。

当用户通过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;
}

3 个答案:

答案 0 :(得分:1)

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

尝试使用map itselt(ACCOUNT_CONTEXT_MAP)而不是每个Account对象进行同步。

如果您不想在地图上进行同步,因为这会导致其他线程等待创建昂贵的AccountContext,请尝试以下操作:

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

答案 1 :(得分:1)

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

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;

答案 2 :(得分:0)

正如@morgano指出的那样,只有当account是所有同等帐户的同一个实例时,这才有效。此外,Map必须是线程安全的 - 这通常意味着使用ConcurrentHashMap或类似的。如果你的地图不是线程安全的,那么第一行的get就不是线程安全的 - 很多坏事都可能出错。

你可以做的一件事是锁定你的锁。创建一个N个对象的数组(字面意思Object很好)。当您需要锁定synchronized块时,从account.hashCode() % locksArray.length获取它,然后在该对象上进行同步。这意味着只要他们的AccountContext具有不同的Account,您就可以并行创建多个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阻止。