0

在某些文章中,我读到双重检查锁定已损坏。因为编译器可以重新排序构造函数的顺序。

  1. ss为一个对象分配内存
  2. 然后将地址返回给引用变量
  3. 然后初始化对象的状态

虽然通常人们会期望:

  1. 它应该是为对象分配的内存
  2. 然后初始化对象的状态
  3. 然后将地址返回给引用变量。

同样,当使用synchronized关键字时,代码重新排序永远不会按照 JMM 规范发生。

为什么编译器在 synchronized() 块内时会重新排序构造函数事件的序列?

我在这里看到了很多关于 DCL 的帖子,但我期待基于 JMM 和编译器重新排序的描述。

4

3 回答 3

3

编译器可以自由地重新排序同步块中的指令。并且编译器可以自由地在同步块之前(只要它们停留在之前)或之后(只要它们停留在之后)重新排序指令。但是,编译器不能自由地同步块边界(块开始或块结束)重新排序指令。

因此,完全在同步块内的构造和赋值可以被重新排序,并且没有正确同步的外部观察者可以看到构造之前的赋值。

于 2013-08-06T03:42:27.553 回答
0

为什么编译器在 synchronized() 块内时会重新排序构造函数事件的序列?

它通常会这样做以使代码运行得更快。

Java 语言规范 (JLS) 表示允许实现(例如,编译器)重新排序指令和受某些约束的指令序列。

问题是 DCL 的损坏变体做出的假设超出了 JLS 所说的可以做出的假设。结果是 JLS 说的执行格式不正确。这是否表现为实际的错误/意外行为取决于编译器版本、硬件和其他各种因素。

但关键是编译器没有做错任何事情。故障在 DCL 代码中。


我只想补充一点,JIT 编译器本身通常不会重新排序事件。它经常做的是消除对硬件级内存读/写操作的限制。例如,通过删除特定内存写入刷新到主内存的约束,您允许硬件推迟(甚至完全跳过)慢速写入内存,而只写入 L1 缓存。相比之下,synchronized块的结尾将强制缓存写入到主内存,从而导致额外的内存流量和(可能)管道停止。

于 2013-08-06T04:32:22.763 回答
0

首先:

同样,当使用 synchronized 关键字时,代码重新排序永远不会按照 JMM 规范发生。

上述说法并不完全准确。JMM 定义了happens-before 关系。JLS 只定义了程序顺序和发生前的顺序。见17.4.5。发生在订单之前

它对指令的重新排序有影响。例如,

int x = 1;
synch(obj) {
    y = 2;
}
int z = 3;

现在对于上面的代码,可以进行以下类型的重新排序。

synch(obj) {
    int x = 1;
    y = 2;
    int z = 3;
}

以上是有效的重新排序。

请参阅Roach Motels 和 Java 内存模型

synch(obj) {
    int z = 3;
    y = 2;
    int x = 1;
}

以上也是有效的重新排序。

不可能的是 y=2 只会在获得锁之后和释放锁之前执行,这是 JMM 所保证的。此外,为了从另一个线程看到正确的效果,我们只需要在同步块内访问 y 。

现在我来到 DCL。

请参阅 DCL 的代码。

if (singleton == null)
    synch(obj) {
        if(singleton == null) {
            singleton == new Singleton()
        }
    }
return singleton;

现在上述方法的问题是:

  1. singleton = new Singleton()不是一条指令。而是一套指令。在完全初始化构造函数之前,很可能首先为单例引用分配对象引用。

  2. 因此,如果1发生,那么其他线程很可能将单例引用读取为非 null,因此看到的是部分构造的对象。

可以通过将单例设置为 volatile 来控制上述影响,这也建立了先发生的保证和可见性。

于 2013-08-06T07:08:26.293 回答