1
1.   class Foo {
2.       private Helper helper = null;
3.       public Helper getHelper() {
4.           if (helper == null) {
5.              synchronized(this) {
6.                 if (helper == null) {
7.                    helper = new Helper();
8.                 }
9.              }
10.          }
11.          return helper;
12.      }
13.  }

这种结构被认为是损坏的原因通常描述为编译器完成的赋值重新排序,以便在写入助手变量后调用助手构造函数。我的问题是,这段代码是如何线程安全的,以下步骤是否可行?

  • 线程 1,进入同步块,发现 helper 为空。
  • 线程1,此时放弃监视器
  • 线程2,进入对象监视器并实例化助手
  • 线程 1,返回并重新初始化助手实例

我看不出这个解决方案比单检查锁定更好。

4

3 回答 3

4

这适用于对助手的引用,但仍然被巧妙地破坏了。

它被破坏了,因为允许虚拟机在同步块中尽可能多地重新排序程序操作因此可以在帮助器实例(由线程 1)构建完成之前将参考帮助器设置为非空。

线程 2 现在可以在同步块之外看到非空,从不尝试进入同步块(线程 1 仍将持有锁并忙于构造 Helper)并使用半构造的 Helper 实例。

这可能会或可能不会发生在特定的 VM 版本上。但是该规范明确允许 VM 执行此操作。这就是示例被破坏的原因。它可以通过声明辅助函数 volatile 来修复(仅适用于 Java 5+)。

于 2014-07-17T18:40:02.630 回答
2

在检查 helper 是否为空后,线程 1 如何放弃监视器?在初始化助手之前,它不会释放锁。

几年前这在 JVM 上不起作用,但他们改变了内存模型并修复了这个问题。

当前的“最佳”方式不是 DCL,而是将单例实现为枚举。

于 2014-07-17T18:23:34.303 回答
0

Helper它仍然被破坏,因为即使Helper实例不为空,也可能不会发布其中任何字段的写入。例如:

class Helper {
   int someField;
   public Helper(){
     someField = 10;
   }
}

在这种情况下,根据 JLS 有可能有类似的东西:

Helper someHelper = getHelper();
if(someHelper.someField == 0){
   // error
}

//error由于 someField 的读写与helperwithin的写入不同步,理论上标记的行可能会被命中getHelper()

于 2014-07-17T19:38:23.453 回答