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

Rom*_*man 15 java multithreading synchronization locking anti-patterns

此问题涉及旧Java版本的行为和双重检查锁定算法的旧实现

较新的实现使用volatile并依赖于稍微改变的volatile语义,因此它们不会被破坏.


据说,除了long或double字段外,字段赋值总是原子的.

但是,当我读到为什么双重检查锁定被破坏的解释时,它说问题在于赋值操作:

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

什么时候可能?是否有可能在64位JVM分配操作中不是原子的?如果不是那么"双重检查锁定"是否真的被打破了?

mer*_*ike 15

问题不在于原子性,而在于它的排序.只要不违反发生之前的情况,JVM就可以重新排序指令以提高性能.因此,运行时理论上可以在执行helper类的构造函数的所有指令之前调度更新的指令Helper.


Rob*_*ert 7

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

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

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