Java Double Locking – 有人可以更直接地解释为什么直觉不起作用?

我在这里找到了以下代码: http : //en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java

我试图理解为什么在某些情况下这不起作用。 我阅读了“微妙”问题的解释,并且使用volatile将解决问题,但我有点困惑。

 // Broken multithreaded version // "Double-Checked Locking" idiom class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized(this) { if (helper == null) { helper = new Helper(); } } } return helper; } // other functions and members... } 

基本上,我是否正确地认为这会失败,因为synchronized块中的helper == null检查有可能失败,因为它可能在那一点“部分”构造? 如果对象是部分构造的,java 返回null吗? 这是问题吗?

无论如何,我知道进行双重检查锁定不是很好的做法,但我只是好奇理论为什么上面的代码失败了,为什么volatile(再加上分配一个局部变量)修复了这个? 这是我从某处获得的一些代码。

 // Double-check idiom for lazy initialization of instance fields private volatile FieldType field; FieldType getField() { FieldType result = field; if (result == null) { // First check (no locking) synchronized(this) { result = field; if (result == null) // Second check (with locking) field = result = computeFieldValue(); } } return result; } 

我知道已有一千个关于此的post,但解释似乎提到了1.5后的内存模型的变化,我也不太明白它与它有什么关系:-(。

提前致谢!

我是否正确地认为这会失败,因为同步块中的辅助器== null检查有可能失败,因为它可能在那一点“部分”构造?

是的,你是对的。 这在无序写入中进行了解释 。 helper = new Helper()由3个步骤组成:内存分配,对构造函数的调用和赋值。 JIT编译器可以自由地重新排序指令并在内存分配(返回对新对象的引用)之后但在构造函数调用之前进行赋值。 使用volatile可防止重新排序。

您需要声明字段volatile因为这将强制写入字段“刷新”到主存储器 。 否则,JVM规范允许每个线程保留其本地版本的字段,并且永远不会将其写入传递给其他线程。 这通常很好,因为它允许在JVM中进行积极的优化。

希望有所帮助! 另外我可以推荐一杯非常浓的咖啡,一个非常安静的房间,然后阅读Java Memory Model ,它解释了它的工作原理以及线程之间的交互。 我想你会惊讶于需要一个线程将其写入(共享)内存传递给其他线程以及JVM可以执行的读写重新排序所需的情况很少!

令人兴奋的阅读!