为什么在 Java 中双重检查锁定被破坏了?

这个问题与旧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...
}
  1. 线程 A 注意到该值未初始化,因此它获取锁并开始初始化该值。
  2. 由于某些编程语言的语义,允许编译器生成的代码在 A 完成执行初始化之前更新共享变量以指向部分构造的对象。
  3. 线程 B 注意到共享变量已初始化(或显示如此),并返回其值。由于线程 B 认为该值已初始化,因此它不会获取锁。如果 B 在 B 看到 A 完成的所有初始化之前使用该对象(因为 A 尚未完成初始化它,或者因为对象中的某些初始化值尚未渗透到 B 使用的内存(缓存一致性)),则程序可能会崩溃。
    (来自 http://en.wikipedia.org/wiki/Double-checked_locking)。

什么时候可能?是否有可能在 64 位 JVM 上分配操作不是原子的?如果不是,那么“双重检查锁定”是否真的坏了?


答案 1

问题不在于原子性,而在于排序。允许 JVM 对指令进行重新排序以提高性能,只要不违反发生之前。因此,从理论上讲,运行时可以安排在类构造函数的所有指令执行之前更新的指令。helperHelper


答案 2

引用的赋值是原子的,但构造不是!因此,如解释中所述,假设线程 B 想要在线程 A 完全构造它之前使用单例,它无法创建新实例,因为引用不是 null,因此它只返回部分构造的对象。

如果不确保在另一个线程加载该共享引用之前发布共享引用,则可以使用对其字段的写入来重新排序对新对象的引用的写入。在这种情况下,另一个线程可以看到对象引用的最新值,但可以看到部分或全部对象状态(部分构造的对象)的过期值。-- Brian Goetz: Java Concurrency in Practice

由于对 null 的初始检查未同步,因此没有发布,并且可以进行这种重新排序。