【问题标题】:With double-checked locking, does a put to a volatile ConcurrentHashMap have happens-before guarantee?使用双重检查锁定,对 volatile ConcurrentHashMap 的 put 是否有发生前的保证?
【发布时间】:2013-06-14 15:51:55
【问题描述】:

到目前为止,我使用的双重检查锁定如下:

class Example {
  static Object o;
  volatile static boolean setupDone;

  private Example() { /* private constructor */ }

  getInstance() {
    if(!setupDone) {
      synchronized(Example.class) {
        if(/*still*/ !setupDone) {
          o = new String("typically a more complicated operation"); 
          setupDone = true;
        }        
      }
    }
    return o;
  }
}// end of class

现在,因为我们有一组线程都共享这个类,我们将boolean 更改为ConcurrentHashMap,如下所示:

class Example {
  static ConcurrentHashMap<String, Object> o = new ConcurrentHashMap<String, Object>();
  static volatile ConcurrentHashMap<String, Boolean> setupsDone = new ConcurrentHashMap<String, Boolean>();

  private Example() { /* private constructor */ }

  getInstance(String groupId) {
    if (!setupsDone.containsKey(groupId)) {
      setupsDone.put(groupId, false);
    }
    if(!setupsDone.get(groupId)) {
      synchronized(Example.class) {
        if(/*still*/ !setupsDone.get(groupId)) {
          o.put(groupId, new String("typically a more complicated operation")); 
          setupsDone.put(groupId, true); // will this still maintain happens-before?
        }        
      }
    }
    return o.get(groupId);
  }
}// end of class

我现在的问题是:如果我将标准 Object 声明为 volatile,我只会在读取或写入其引用时建立起之前发生的关系。因此,在该对象中写入一个元素(例如,如果它是标准的 HashMap,则对其执行 put() 操作)将不会建立这样的关系。 正确吗?(读取一个元素呢;那不是也需要读取引用并因此建立关系吗?)

现在,使用 volatile ConcurrentHashMap,将向其写入一个元素建立发生前的关系,即上述方法仍然有效

更新:这个问题的原因以及为什么双重检查锁定很重要: 我们实际设置的(而不是对象)是MultiThreadedHttpConnectionManager,我们将一些设置传递给它,然后我们将其传递给HttpClient,我们也设置它,然后返回。我们有多达 10 个组,每组最多 100 个线程,我们使用双重检查锁定,因为我们不想在需要获取组的 HttpClient 时阻止每个组,因为整个设置将用于帮助进行性能测试。由于笨拙的设计和奇怪的平台,我们不能只从外部传入对象,所以我们希望以某种方式使这个设置工作。 (我意识到问题的原因有点具体,但我希望问题本身足够有趣:有没有办法让ConcurrentHashMap 使用“易变行为”,即建立一个发生- 之前的关系,就像volatile boolean 所做的那样,在ConcurrentHashMap 上执行put() 时? ;)

【问题讨论】:

    标签: java multithreading volatile concurrenthashmap happens-before


    【解决方案1】:

    是的,没错。 volatile 仅保护该对象引用,而没有其他保护。

    不,将元素放入 volatile HashMap 不会创建发生前的关系,即使是 ConcurrentHashMap 也不会。

    实际上ConcurrentHashMap 并没有为读操作持有锁(例如containsKey())。请参阅ConcurrentHashMapJavadoc。

    更新

    反映您更新的问题:您必须在放入 CHM 的对象上进行同步。我建议使用容器对象,而不是直接将Object 存储在地图中:

    public class ObjectContainer {
        volatile boolean isSetupDone = false;
        Object o;
    }
    
    static ConcurrentHashMap<String, ObjectContainer> containers = 
        new ConcurrentHashMap<String, ObjectContainer>();
    
    public Object getInstance(String groupId) {
      ObjectContainer oc = containers.get(groupId);
      if (oc == null) {
        // it's enough to sync on the map, don't need the whole class
        synchronized(containers) {
          // double-check not to overwrite the created object
          if (!containers.containsKey(groupId))
            oc = new ObjectContainer();
            containers.put(groupId, oc);
          } else {
            // if another thread already created, then use that
            oc = containers.get(groupId);
          }
        } // leave the class-level sync block
      }
    
      // here we have a valid ObjectContainer, but may not have been initialized
    
      // same doublechecking for object initialization
      if(!oc.isSetupDone) {
        // now syncing on the ObjectContainer only
        synchronized(oc) {
          if(!oc.isSetupDone) {
            oc.o = new String("typically a more complicated operation"));
            oc.isSetupDone = true;
          }        
        }
      }
      return oc.o;
    }
    

    注意,在创建时,最多一个线程可以创建ObjectContainer。但是在初始化时,每个组可以并行初始化(但每个组最多 1 个线程)。

    也有可能Thread T1会创建ObjectContainer,但Thread T2会初始化它。

    是的,值得保留ConcurrentHashMap,因为映射读取和写入将同时发生。但volatile 不是必需的,因为地图对象本身不会改变。

    可悲的是,双重检查并不总是有效,因为编译器可能会创建一个字节码,它正在重用 containers.get(groupId) 的结果(这不是 volatile isSetupDone 的情况)。这就是为什么我必须使用containsKey 进行双重检查。

    【讨论】:

    • 您好 GaborSch,谢谢您的建议;看起来真的很棒。不过,只有一个问题:当您说“易失性 isSetupDone 不是这种情况”时,您的意思是我们应该像这样:isSetupDonevolatilevolatile boolean isSetupDone = false;
    • @Christian 没错。对不起,这是我的错误,将更新该行。我的意思是isSetupDonevolatile
    【解决方案2】:

    因此,在该对象中写入一个元素(如果它是例如标准的 HashMap,对其执行 put() 操作)将不会建立这种关系。对吗?

    是和不是。当您读取或写入 volatile 字段时,始终存在发生前的关系。您的情况的问题是,即使在您访问 HashMap 字段时发生了之前发生的情况,当您实际操作 HashMap 时,也没有内存同步或互斥锁。因此,多个线程可以看到同一 HashMap 的不同版本,并且可以根据竞争条件创建损坏的数据结构。

    现在,通过使用 volatile ConcurrentHashMap,将向其写入元素会建立起之前的关系,即上述方法仍然有效吗?

    通常您不需要需要将ConcurrentHashMap 标记为volatileConcurrentHashMap 代码本身内部存在内存障碍。我唯一会使用它是如果ConcurrentHashMap 字段经常被更改——即不是最终的。

    您的代码看起来确实是过早的优化。探查器是否向您表明这是一个性能问题?我建议你只在地图上同步,我就完成了。有两个ConcurrentHashMap 来解决这个问题对我来说似乎有点过头了。

    【讨论】:

    • 感谢您的回答!我刚刚写了一个很长的评论,解释了为什么我认为我需要双重检查锁定开始,只是为了意识到我不允许在这里发布任何内容。我会在几个小时内回复你。暂时,让我把这个问题抛回到讨论中:有没有办法让ConcurrentHashMap 在对其执行put() 时使用“volatile 行为”? ;)
    • ConcurrentHashMap 是完全并发的。如果您只是通过调用setupsDone.put(...) 来询问是否存在发生之前的关系,那么答案是肯定的,但您不知道何时跨越内存障碍。您确实知道,当推杆完成时,一个已经被越过了。
    • 这种发生在关系/内存障碍之前是否会影响ConcurrentHashMap 或者 - 通常情况下 - 在它发生之前对写入线程可见的所有变量? (即写setupsDone会影响o在其他线程中的可见性吗?)
    • 这个周末我查看了ConcurrentHashMap 的源代码并试图理解它。 Java 6 中的ConcurrentHashMap 有一个transient volatile 字段count,如果(且仅当)使用put() 添加新密钥时,该字段显然被get() 方法读取并写入,因此建立了一个happens-之前的关系,我想。 (我没有检查“覆盖”。)在 Java 7 中,count 不再声明为volatile,而是使用transient volatile HashEntry&lt;K,V&gt;[] table; 代替(我没有详细研究其用途)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-12
    • 2020-03-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多