2013-06-30 125 views
2

我有一个Web应用程序。运行在tomcat和多个线程服务Servlet调用。Java线程安全延迟初始化

我有一个User类,Account类和一个1AccountContext类。可以有多个Users

根据Account,只应在内存中保留AccountContext的1个实例。

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

回答

1

恕我直言,这不是线程安全的,除非你能保证所有线程都拥有帐户的同一个实例,有没有办法有两个帐户对象表示相同的“账户”,请考虑以下情况:两个线程每个都有一个Account对象,表示同一个帐户,他们都调用getAccountContext(),第一个线程在if(accountContext == null)之后但在开始初始化之前被挂起,然后第二个线程得到相同的结果, accountContext为空并继续创建AccountContext,则第一个线程再次被给予CPU时间,因为此第一个线程已经“验证”了accountContext为null,它将继续创建另一个实例。

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

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

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

是的,你说得对。我不能保证它们是同一个Account对象。但我也不想在地图上进行同步,因为我不希望线程在不同的帐户上等待昂贵的AccountContext创建。如果不同帐户的上下文创建并行完成,并且仅在添加到散列映射时才支付同步惩罚,那么我希望这样做。 – golfmonke

+0

@ user2537426我编辑了我的帖子,为您的问题添加了可能的解决方案,yshavit的解决方案也是一个不错的解决方案。 – morgano

0

由于@morgano指出,如果account是人人享有平等帐户相同的实例这仅适用。此外,Map必须是线程安全的 - 这通常意味着使用ConcurrentHashMap或类似的。如果你的地图不是线程安全的,那么第一行的get就不是线程安全的 - 很多不好的事情都会出错。

你可以做的一件事是锁定你的锁。创建一个N对象数组(字面上Object是好的)。当您需要锁定​​块时,请从account.hashCode() % locksArray.length获取该锁,然后在该对象上进行同步。这意味着您可以并行创建多个AccountContext,只要它们的Account具有不同的hashCode() % N即可。平均而言,这应该会给你良好的表现;显然它假定Account有一个合适的覆盖。

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区块。

+1

在guava中有一个很好的条纹锁实现:com.google.common.util.concurrent.Striped http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/util/concurrent /Striped.html – Keith

+0

谢谢你的建议,看起来像我需要的。我的帐户哈希码基于我在帐户创建时分配的UUID。基于你的建议,我发现并检出了Guava系列中的Striped类。你看到使用你的方法与Striped有什么折衷,它返回一个java util并发锁吗? **之前我看到基思的回复...... – golfmonke

+0

我不知道那个班,很酷。 – yshavit

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; 
+0

我也试图避免让多个线程开始昂贵的AccountContext创建。 – golfmonke