【问题标题】:Why is double-checked locking broken in Java?为什么在 Java 中双重检查锁定被破坏?
【发布时间】:2011-02-07 21:15:47
【问题描述】:

这个问题与旧 Java 版本的行为和双重检查锁定算法的旧实现有关

较新的实现 use 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...
}
  1. 线程 A 注意到该值未初始化,因此它获取 锁定并开始初始化 价值。
  2. 由于某些编程语言的语义,代码 允许编译器生成 更新共享变量指向 到一个部分构造的对象 在 A 完成执行之前 初始化。
  3. 线程 B 注意到共享变量已被初始化(或 它出现),并返回其值。 因为线程 B 认为值是 已经初始化,它没有 获取锁。如果 B 使用对象 在所有初始化完成之前 A 被 B 看到(或者因为 A 尚未完成初始化或 因为一些初始化值 在物体中尚未渗出 到内存 B 使用(缓存 连贯性)),该程序可能 碰撞。
    (来自http://en.wikipedia.org/wiki/Double-checked_locking)。

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

【问题讨论】:

标签: java multithreading synchronization locking anti-patterns


【解决方案1】:

问题不在于原子性,而在于顺序。只要不违反happens-before,JVM 就可以重新排序指令以提高性能。因此,运行时理论上可以在来自类Helper的构造函数的所有指令执行之前调度更新helper的指令。

【讨论】:

    【解决方案2】:

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

    如果您不确保发布 共享参考发生在之前 另一个线程加载共享 参考,然后写的 对新对象的引用可以是 重新排序与写入其 字段。在这种情况下,另一个线程 可以看到的最新值 对象引用但已过时 对象的部分或全部值 state - 部分构造的 目的。 -- Brian Goetz:Java 并发实践

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

    【讨论】:

    • 是否在任何地方都说明java首先进行分配,然后才计算正确的部分? (这有点像Future 模式,但我从未见过有关 java 编译器使用它的官方声明)。
    • @Roman:这不是“Java 先赋值,然后才计算正确部分”的简单问题。我认为那将是一个不正确的陈述。当另一个线程访问不是volatilefinal 的共享字段时,Java 允许低级代码以一种可能导致意外结果的方式重新排序语句。
    【解决方案3】:

    在构造函数中构造 Helper 的实例可能需要多个赋值,并且语义允许它们相对于赋值 helper = new Helper() 重新排序。

    因此,helper 字段可能会被分配一个对并非所有分配都发生的对象的引用,因此它没有完全初始化。

    【讨论】:

    • 你是什么意思“几个任务”?我只看到 1 个:helper = new Helper()。如果可以“就地”构建它,那么您能否提供一个链接来证明 java 确实做了如此奇怪的事情?谢谢。
    • @Roman:正如您问题中的引号中所述,编译器生成的代码允许将Helper 分配给尚未完成构造的Helper 实例 i>.
    【解决方案4】:

    java中的双重检查锁存在多种问题:

    http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

    【讨论】:

    • 对于 Java 5+,如果您声明初始化为 volatile 的字段(当然如该论文中所述),它可以正常工作,没有问题。
    【解决方案5】:

    阅读这篇文章:http://www.javaworld.com/jw-02-2001/jw-0209-double.html 即使你不了解所有细节(像我一样),也要相信这个好把戏是行不通的。

    【讨论】:

      【解决方案6】:

      对不起,这可能与问题无关,我只是好奇。 在这种情况下,在分配和/或返回值之前获取锁不是更好吗?喜欢:

      private Lock mLock = new ReentrantLock();
      private Helper mHelper = null;
      
      private Helper getHelper() {
          mLock.lock();
          try {
              if (mHelper == null) {
                  mHelper = new Helper();
              }
              return mHelper;
          }
          finally {
              mLock.unlock();
          }
      }
      

      或者使用双重检查锁定有什么好处?

      【讨论】:

        【解决方案7】:
        /*Then the following should work.
          Remember: getHelper() is usually called many times, it is BAD 
          to call synchronized() every time for such a trivial thing!
        */
        class Foo {
        
        private Helper helper = null;
        private Boolean isHelperInstantiated;
        public Helper getHelper() {
            if (!isHelperInstantiated) {
                synchronized(this) {
                    if (helper == null) {
                        helper = new Helper();
                        isHelperInstantiated = true;
                    }
                }
            }
            return helper;
        }
        
        // other functions and members...
        }    
        

        【讨论】:

          猜你喜欢
          • 2011-08-22
          • 1970-01-01
          • 1970-01-01
          • 2011-12-12
          • 2021-10-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多