Java 使用双重检查锁定时,在保证之前是否对易失性ConcurrentHashMap执行put操作?

Java 使用双重检查锁定时,在保证之前是否对易失性ConcurrentHashMap执行put操作?,java,multithreading,volatile,concurrenthashmap,happens-before,Java,Multithreading,Volatile,Concurrenthashmap,Happens Before,到目前为止,我使用了双重检查锁定,如下所示: class Example { static Object o; volatile static boolean setupDone; private Example() { /* private constructor */ } getInstance() { if(!setupDone) { synchronized(Example.class) { if(/*still*/ !setupD

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

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
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
现在,由于我们有共享该类的线程组,我们将布尔值更改为ConcurrentHashMap,如下所示:

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
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
我现在的问题是:如果我将一个标准对象声明为volatile,那么当我读或写它的引用时,我只会得到一个before关系。因此,如果在该对象中写入元素(例如,标准HashMap),则对其执行put操作不会建立这种关系。对吗?读一个元素怎么样;这难道不需要阅读参考资料,从而建立关系吗

现在,通过使用volatile ConcurrentHashMap,向其中写入一个元素是否会建立“发生在之前”关系,即上述关系是否仍然有效

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

是的,这是正确的。volatile只保护该对象引用,而不保护其他对象

不,将元素放入volatile HashMap不会创建“发生在之前”关系,即使使用ConcurrentHashMap也不会

实际上,ConcurrentHashMap并没有为读取操作(例如containsKey)持有锁。见Javadoc

更新:

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

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个线程

线程T1也可能会创建ObjectContainer,但线程T2会初始化它

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


可悲的是,双重检查并不总是有效的,因为编译器可能会创建一个字节码来重用containers.getgroupId的结果,而volatile isSetupDone的情况并非如此。这就是为什么我必须使用containsKey进行双重检查。

是的,它是正确的。volatile只保护该对象引用,而不保护其他对象

不,将元素放入volatile HashMap不会创建“发生在之前”关系,即使使用ConcurrentHashMap也不会

实际上,ConcurrentHashMap并没有为读取操作(例如containsKey)持有锁。见Javadoc

更新:

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

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个线程

线程T1也可能会创建ObjectContainer,但线程T2会初始化它

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

可悲的是,双重检查并不总是有效的,因为编译器可能会创建一个字节码来重用containers.getgroupId的结果,而volatile isSetupDone的情况并非如此。这就是为什么我必须使用containsKey进行双重检查

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

是和否。读取或写入易变字段时,始终存在“发生在”关系。本例中的问题是,即使在访问HashMap字段之前发生了错误,但在实际操作HashMap时,没有内存同步或互斥锁。因此,多个线程可以创建多个线程 ee同一HashMap的不同版本,并可能根据竞争条件创建损坏的数据结构

现在,通过使用volatile ConcurrentHashMap,向其中写入一个元素是否会建立“发生在之前”关系,即上述关系是否仍然有效

通常,您不需要将ConcurrentHashMap标记为易失性。ConcurrentHashMap代码本身内部存在跨越的内存障碍。如果ConcurrentHashMap字段经常被更改,那么我唯一会使用它,即它是非最终字段

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

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

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

现在,通过使用volatile ConcurrentHashMap,向其中写入一个元素是否会建立“发生在之前”关系,即上述关系是否仍然有效

通常,您不需要将ConcurrentHashMap标记为易失性。ConcurrentHashMap代码本身内部存在跨越的内存障碍。如果ConcurrentHashMap字段经常被更改,那么我唯一会使用它,即它是非最终字段


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

谢谢你的回答!我刚刚写了一篇很长的评论,解释了为什么我认为我需要从双重检查锁定开始,只是为了意识到我不允许在这里发布太长的内容。我过几个小时再给你打电话。目前,让我把这个问题抛回到讨论中:有没有办法让ConcurrentHashMap在执行put时使用volatile行为;ConcurrentHashMap是完全并发的。如果您通过调用setupsDone.put来询问是否存在“先发生后发生”关系。。。答案是肯定的,但你不知道什么时候跨越了记忆的障碍。当put完成时,您确实知道有一个变量被跨越了。这种情况会发生在关系/内存障碍只影响ConcurrentHashMap之前,还是会像通常一样影响写入线程在发生之前可见的所有变量之前?也就是说,写入setupsDone会影响o在其他线程中的可见性吗?本周末我查看了ConcurrentHashMap的源代码,并试图理解它。Java 6中的ConcurrentHashMap有一个瞬时的volatile字段计数,它显然由get方法读取,并且仅当使用put添加了一个新键时才写入,因此我认为建立了一个before-before关系。我没有检查覆盖。在Java7中,count不再声明为volatile,而是瞬态volatile HashEntry[]表;我没有详细检查它的用法。谢谢你的回答!我刚刚写了一篇很长的评论,解释了为什么我认为我需要从双重检查锁定开始,只是为了意识到我不允许在这里发布太长的内容。我过几个小时再给你打电话。目前,让我把这个问题抛回到讨论中:有没有办法让ConcurrentHashMap在执行put时使用volatile行为;ConcurrentHashMap是完全并发的。如果您通过调用setupsDone.put来询问是否存在“先发生后发生”关系。。。答案是肯定的,但你不知道什么时候跨越了记忆的障碍。当put完成时,您确实知道有一个变量被跨越了。这种情况会发生在关系/内存障碍只影响ConcurrentHashMap之前,还是会像通常一样影响写入线程在发生之前可见的所有变量之前?也就是说,写入setupsDone会影响o在其他线程中的可见性吗?本周末我查看了ConcurrentHashMap的源代码,并试图理解它。Java 6中的ConcurrentHashMap有一个瞬时的volatile字段计数,它显然由get方法读取,并且仅当使用put添加了一个新键时才写入,因此我认为建立了一个before-before关系。我没有检查覆盖。在Java7中,counter不再声明为volatile,而是transie
nt volatile HashEntry[]表;我没有详细研究它的用法。嗨,加博什,谢谢你的建议;看起来真的很棒。不过,只有一个问题:当你说volatile的isSetupDone不是这样的时候,你的意思是我们应该让isSetupDone volatile像这样:volatile布尔值isSetupDone=false@没错。对不起,这是我的错误,我要更新那一行。我的意思是我被设置成易变的。嗨,加博什,谢谢你的建议;看起来真的很棒。不过,只有一个问题:当你说volatile的isSetupDone不是这样的时候,你的意思是我们应该让isSetupDone volatile像这样:volatile布尔值isSetupDone=false@没错。对不起,这是我的错误,我要更新那一行。我的意思是说我被设置成易变的。