`

“双重检查锁定被打破”的声明

阅读更多

“双重检查锁定被打破”的声明
The "Double-Checked Locking is Broken" Declaration

Signed by: David Bacon (IBM Research) Joshua Bloch (Javasoft), Jeff Bogda, Cliff Click (Hotspot JVM project), Paul Haahr, Doug Lea, Tom May, Jan-Willem Maessen, Jeremy Manson, John D. Mitchell (jGuru) Kelvin Nilsen, Bill Pugh, Emin Gun Sirer
翻译 彭强兵

在多线程环境下实现延迟加载时,双重检查锁定是广泛使用的而且高效的方法。

很可惜,如果没有额外的同步机制,它也许不能在java平台可靠地运行。当用其他语言实现,例如C++,这依赖于处理器的内存模型、编译器执行的重排序、编译器和同步库之间的相互作用。因为在诸如C++的语言中这些都没有约定,很难说清什么情况会正常运行。在C++中通过显示指明内存屏障来保证正常运行,但内存屏障在java中不可用。

先解释所期望的行为,考虑如下代码:

// Single threaded version
class Foo {
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null)
        helper = new Helper();
    return helper;
    }
  // other functions and members...
  }

如果这段代码在多线程环境下运行,会有很多问题。最明显的问题是,两个或多个Helper对象被创建。(稍后我们会讲述其他问题)。简单的解决方法是给getHelper()方法加上同步。

// Correct multithreaded version
class Foo {
  private Helper helper = null;
  public synchronized Helper getHelper() {
    if (helper == null)
        helper = new Helper();
    return helper;
    }
  // other functions and members...
  }

上面这段代码,每次调用getHelper()时都会进行同步。双重检查锁定试图避免在helper对象被创建后的同步。

// 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...
  }

很可惜,在优化编译器或者共享内存的多处理器计算机上,前面的代码不能正常运行。

不能正常运行

不能正常运行有很多原因。我们描述的第一对原因很明显。理解了这些,你可能试图想办法“解决”双重检查锁定存在的问题。但你的解决方案不起作用:因为有很微妙的原因。理解这些原因后,会提出更好的解决方案,但解决方案还是有问题,因为还有更微妙的原因。

许多非常聪明的人花了大量的时间关注这个问题。但是除了让每个线程同步访问helpser对象外没有办法解决这个问题。

不能正常运行的第一个原因

不能正常运行最明显的原因是,初始化Helper对象和helpser字段的赋值没有按顺序去做完。因此,调用getHelper()方法的线程,已经拥有helper对象的非空引用,但看到helper对象的字段的默认值,而不是构造函数里设置的值。

如果编译器内联构造函数的调用且能证明构造函数不抛出异常或不执行同步,那么初始化对象和对象字段的赋值是可以自由的调整顺序的。

即使编译器不调整这些写操作的顺序,在多处理器计算机上,如果一个线程在另一个处理器上运行,处理器或内存系统也可能调整这些写操作的顺序。

Doug Lea 详细描述了基于编译的调整顺序。

表明不能正常运行的测试案例

Paul Jakubik给出了使用双重检查锁定不能正确运行的例子。稍微简化的代码如下。

当在使用Symantec JIT(just-in-time )编译器的系统上运行程序,程序不能正常运行。

如下所示(注意Symantec JIT 使用基于句柄的对象分配系统)

0206106A   mov         eax,0F97E78h
0206106F   call        01F6B210                  ; allocate space for
                                                 ; Singleton, return result in eax
02061074   mov         dword ptr [ebp],eax       ; EBP is &singletons[i].reference
                                                ; store the unconstructed object here.
02061077   mov         ecx,dword ptr [eax]       ; dereference the handle to
                                                 ; get the raw pointer
02061079   mov         dword ptr [ecx],100h      ; Next 4 lines are
0206107F   mov         dword ptr [ecx+4],200h    ; Singleton's inlined constructor
02061086   mov         dword ptr [ecx+8],400h
0206108D   mov         dword ptr [ecx+0Ch],0F84030h

正如你所见到的,给singletons[i].reference赋值在Singleton构造器被调用之前执行。在现有的java内存模型里这是完全合法的,在C和C++里也是合法的(因为它们都没有自己的内存模型)

不能正常运行的解决方法

鉴于上述解释,有人建议这样编码:
// (Still) Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) {
      Helper h;
      synchronized(this) {
        h = helper;
        if (h == null)
            synchronized (this) {
              h = new Helper();
            } // release inner synchronization lock
        helper = h;
        }
      }   
    return helper;
    }
  // other functions and members...
  }

这段代码把Helper对象的构造放到最里面的同步块里。这里认为在同步块被释放时应该有内存屏障(memory barrier), 以阻止初始化Helper对象和给helper字段赋值的顺序颠倒。

很可惜,这种想法是绝对错误的。同步的规则不是这样的。退出监视器(释放同步)的规则是,所有退出监视器之前的动作必须在释放监视器之前执行。然而,并没有规定说,退出监视器之后的动作不可以在释放监视器之前执行。也就是说同步块里的代码必须在退出同步时完成,而同步块后面的代码则可以被编译器或运行时环境移到同步块中执行。对于编译器,将instance = temp移动到最里层的同步块内完全合法,也合理。这样就出现了上个版本同样的问题。很多处理器提供执行这种单向内存屏障的指令。但如果改变语义,要求释放锁为一个完整内存屏障会带来性能损失。

不能正常运行的更多修改

你可以强制写操作执行全双向内存屏障。但这是粗放的,低效的,并且,一旦java内存模型改变,几乎无法保证工作。不要这样用,我专门为这个技术写了一篇文章,不要这样用。

然而,即使初始化helper对象的线程执行一个完整内存屏障,它仍然不能正常运行。

这是因为在一些操作系统里,看到helper字段非空值的线程同样需要执行内存屏障

为什么?因为处理器有它们自己的本地内存拷贝。有一些处理器,除非处理器执行缓存一致的指令(例如内存屏障),否则可能读到脏的本地内存拷贝,即使其他处理器使用内存屏障来强制写入全主内存。

我已经写了几篇web文章来讨论,在alpha处理器上这种情况是怎么发生的。

麻烦值得吗?

大部分程序,简单的同步getHelper()代价并不高。只有当你认为同步getHelper()会引起巨大的开销时,你才应该考虑采用这种详细的优化方案。

很多时候,更聪明的方法是使用内建的归并排序而不是处理交换排序会更有效。

静态单例下正常运行

如果你创建的单例是静态的(例如,只有一个Helper对象被创建),相对于另一种对象属性(例如,每一个Foo对象都有一个Helper对象)则有一个简单优雅的解决方案。

在一个独立的类里定义单例作为静态字段。java语义会保证字段被引用后才被初始化,访问字段的任何线程都会看到初始化字段后写入的结果。

class HelperSingleton {
  static Helper singleton = new Helper();
  }

32位原始类型的值能正常运行。

尽管双重检查锁定不能被用到对象引用上,但它可用在32位原始类型的值上(例如,int型或float型)。注意,long型和double型是不可以的,因为64位原始类型不同步的读写不被保证是原子的。

// Correct Double-Checked Locking for 32-bit primitives
class Foo {
  private int cachedHashCode = 0;
  public int hashCode() {
    int h = cachedHashCode;
    if (h == 0)
    synchronized(this) {
      if (cachedHashCode != 0) return cachedHashCode;
      h = computeHashCode();
      cachedHashCode = h;
      }
    return h;
    }
  // other functions and members...
  }

事实上,假设computeHashCode()函数总是返回同样的结果且没有副作用(例如,幂等),你甚至可以删除所有的同步。

// Lazy initialization 32-bit primitives
// Thread-safe if computeHashCode is idempotent
class Foo {
  private int cachedHashCode = 0;
  public int hashCode() {
    int h = cachedHashCode;
    if (h == 0) {
      h = computeHashCode();
      cachedHashCode = h;
      }
    return h;
    }
  // other functions and members...
  }

使用显示的内存屏障

如果有明确的内存屏障指令,双重检查锁定模式正常运行是有可能的。例如,如果用c++编程,可以使用Doug Schmidt 等人书中的代码:

// C++ implementation with explicit memory barriers
// Should work on any platform, including DEC Alphas
// From "Patterns for Concurrent and Distributed Objects",
// by Doug Schmidt
template <class TYPE, class LOCK> TYPE *
Singleton<TYPE, LOCK>::instance (void) {
    // First check
    TYPE* tmp = instance_;
    // Insert the CPU-specific memory barrier instruction
    // to synchronize the cache lines on multi-processor.
    asm ("memoryBarrier");
    if (tmp == 0) {
        // Ensure serialization (guard
        // constructor acquires lock_).
        Guard<LOCK> guard (lock_);
        // Double check.
        tmp = instance_;
        if (tmp == 0) {
                tmp = new TYPE;
                // Insert the CPU-specific memory barrier instruction
                // to synchronize the cache lines on multi-processor.
                asm ("memoryBarrier");
                instance_ = tmp;
        }
    return tmp;
    }

使用Thread Local 存储解决双重检查锁定问题

Alexander Terekhov 提出了一个聪明的建议:用thread local 存储实现双重检查锁定。每个线程保留线程本地标志位,决定线程是否已完成所需的同步

class Foo {
  /** If perThreadInstance.get() returns a non-null value, this thread
  has done synchronization needed to see initialization
  of helper */
         private final ThreadLocal perThreadInstance = new ThreadLocal();
         private Helper helper = null;
         public Helper getHelper() {
             if (perThreadInstance.get() == null) createHelper();
             return helper;
         }
         private final void createHelper() {
             synchronized(this) {
                 if (helper == null)
                     helper = new Helper();
             }
      // Any non-null value would do as the argument here
             perThreadInstance.set(perThreadInstance);
         }
 }

这种技术的性能非常依赖jdk的版本,在jdk1.2里,ThreadLocal's 是非常慢的。1.3里明显快了很多,并且1.4里被期望更快。Doug Lea 分析了实现延迟加载的一些技术的性能。

新的java内存模型

在JDK5里,有新的java内存模型和线程规范。

用Volatile解决双重检查锁定问题

JDK5及以后的版本扩展了volatile的语义,这样系统不允许volatile变量的写操作和前面的读写操作调整顺序,也不允许volatile变量的读操作和后面的读写操作调整顺序。Jeremy Manson's 的博客里可以看到这个条目的更详细的介绍。

因为这个变化,通过声明helper字段为volatile保证双重检查锁定正常运行,但在jdk4及之前不能正常运行。

// Works with acquire/release semantics for volatile
// Broken under current semantics for volatile
  class Foo {
        private volatile Helper helper = null;
        public Helper getHelper() {
            if (helper == null) {
                synchronized(this) {
                    if (helper == null)
                        helper = new Helper();
                }
            }
            return helper;
        }
    }

双重检查锁定不可变对象

如果Helper是不可变对象,就是说Helper的所有字段是final的,那么双重检查锁定不需要使用volatile就可以正常运行。不可变对象的引用(例如String或Integer)应该和int或float有类似的行为。读写不可变对象的引用是原子的。

Descriptions of double-check idiom
Reality Check, Douglas C. Schmidt, C++ Report, SIGS, Vol. 8, No. 3, March 1996.
Double-Checked Locking: An Optimization Pattern for Efficiently Initializing and Accessing Thread-safe Objects, Douglas Schmidt and Tim Harrison. 3rd annual Pattern Languages of Program Design conference, 1996
Lazy instantiation, Philip Bishop and Nigel Warren, JavaWorld Magazine
Programming Java threads in the real world, Part 7, Allen Holub, Javaworld Magazine, April 1999.
Java 2 Performance and Idiom Guide, Craig Larman and Rhett Guthrie, p100.
Java in Practice: Design Styles and Idioms for Effective Java, Nigel Warren and Philip Bishop, p142.
Rule 99, The Elements of Java Style, Allan Vermeulen, Scott Ambler, Greg Bumgardner, Eldon Metz, Trvor Misfeldt, Jim Shur, Patrick Thompson, SIGS Reference library
Global Variables in Java with the Singleton Pattern, Wiebe de Jong, Gamelan

 

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

分享到:
评论

相关推荐

    大厂面试-腾讯,2022年最新资源,祝您斩获高薪offer!

    8. **单例模式**:常见的单例实现有饿汉式、懒汉式、双重检查锁定和静态内部类。每种方式在多线程环境下的安全性、性能和延迟加载效果有所不同。 9. **IO模型**:包括阻塞IO(BIO)、非阻塞IO(NIO)和异步IO(AIO...

    C#单例模式详解 C#单例模式详解C#单例模式详解

    4. 双重检查锁定(DCL,线程安全): 使用volatile关键字保证线程可见性和避免指令重排序,确保多线程环境下的安全性。 ```csharp public class Singleton { private static volatile Singleton instance; ...

    JAVA 面试题总览(书签完整版)

    17. **单例模式**:常见的单例实现有饿汉式、懒汉式、双重检查锁定和静态内部类等。 18. **equals和hashCode**:在重写equals时通常需要同时重写hashCode,以保持一致性。它们在HashSet和HashMap等容器中用于确定...

    避开10大常见坑:DeepSeekAPI集成中的错误处理与调试指南.pdf

    在日常的工作和学习中,你是否常常为处理复杂的数据、生成高质量的文本或者进行精准的图像识别而烦恼?DeepSeek 或许就是你一直在寻找的解决方案!它以其高效、智能的特点,在各个行业都展现出了巨大的应用价值。然而,想要充分发挥 DeepSeek 的优势,掌握从入门到精通的知识和技能至关重要。本文将从实际应用的角度出发,为你详细介绍 DeepSeek 的基本原理、操作方法以及高级技巧。通过系统的学习,你将能够轻松地运用 DeepSeek 解决实际问题,提升工作效率和质量,让自己在职场和学术领域脱颖而出。现在,就让我们一起开启这场实用又高效的学习之旅吧!

    前端分析-2023071100789

    前端分析-2023071100789

    基于kinect的3D人体建模C++完整代码.cpp

    基于kinect的3D人体建模C++完整代码.cpp

    搞机工具箱10.1.0.7z

    搞机工具箱10.1.0.7z

    GRU+informer时间序列预测(Python完整源码和数据)

    GRU+informer时间序列预测(Python完整源码和数据),python代码,pytorch架构,适合各种时间序列直接预测。 适合小白,注释清楚,都能看懂。功能如下: 代码基于数据集划分为训练集测试集。 1.多变量输入,单变量输出/可改多输出 2.多时间步预测,单时间步预测 3.评价指标:R方 RMSE MAE MAPE,对比图 4.数据从excel/csv文件中读取,直接替换即可。 5.结果保存到文本中,可以后续处理。 代码带数据,注释清晰,直接一键运行即可,适合新手小白。

    性价比革命:DeepSeekAPI成本仅为GPT-4的3%的技术揭秘.pdf

    在日常的工作和学习中,你是否常常为处理复杂的数据、生成高质量的文本或者进行精准的图像识别而烦恼?DeepSeek 或许就是你一直在寻找的解决方案!它以其高效、智能的特点,在各个行业都展现出了巨大的应用价值。然而,想要充分发挥 DeepSeek 的优势,掌握从入门到精通的知识和技能至关重要。本文将从实际应用的角度出发,为你详细介绍 DeepSeek 的基本原理、操作方法以及高级技巧。通过系统的学习,你将能够轻松地运用 DeepSeek 解决实际问题,提升工作效率和质量,让自己在职场和学术领域脱颖而出。现在,就让我们一起开启这场实用又高效的学习之旅吧!

    基于ANSYS LSDyna的DEM-SPH-FEM耦合模拟滑坡入水动态行为研究,基于ANSYS LSDyna的DEM-SPH-FEM耦合的滑坡入水模拟分析研究,基于ansys lsdyna的滑坡入水

    基于ANSYS LSDyna的DEM-SPH-FEM耦合模拟滑坡入水动态行为研究,基于ANSYS LSDyna的DEM-SPH-FEM耦合的滑坡入水模拟分析研究,基于ansys lsdyna的滑坡入水模拟dem-sph-fem耦合 ,基于ANSYS LSDyna; 滑坡入水模拟; DEM-SPH-FEM 耦合,基于DEM-SPH-FEM耦合的ANSYS LSDyna滑坡入水模拟

    auto_gptq-0.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

    auto_gptq-0.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

    复件 复件 建设工程可行性研究合同[示范文本].doc

    复件 复件 建设工程可行性研究合同[示范文本].doc

    13考试真题最近的t64.txt

    13考试真题最近的t64.txt

    Microsoft Visual C++ 2005 SP1 Redistributable PackageX86

    好用我已经解决报错问题

    嵌入式开发入门:用C语言点亮LED灯的全栈开发指南.pdf

    # 踏入C语言的奇妙编程世界 在编程的广阔宇宙中,C语言宛如一颗璀璨恒星,以其独特魅力与强大功能,始终占据着不可替代的地位。无论你是编程小白,还是有一定基础想进一步提升的开发者,C语言都值得深入探索。 C语言的高效性与可移植性令人瞩目。它能直接操控硬件,执行速度快,是系统软件、嵌入式开发的首选。同时,代码可在不同操作系统和硬件平台间轻松移植,极大节省开发成本。 学习C语言,能让你深入理解计算机底层原理,培养逻辑思维和问题解决能力。掌握C语言后,再学习其他编程语言也会事半功倍。 现在,让我们一起开启C语言学习之旅。这里有丰富教程、实用案例、详细代码解析,助你逐步掌握C语言核心知识和编程技巧。别再犹豫,加入我们,在C语言的海洋中尽情遨游,挖掘无限可能,为未来的编程之路打下坚实基础!

    auto_gptq-0.4.2-cp38-cp38-win_amd64.whl

    auto_gptq-0.4.2-cp38-cp38-win_amd64.whl

    自动立体库设计方案.pptx

    自动立体库设计方案.pptx

    手把手教你用C语言实现贪吃蛇游戏:从算法设计到图形渲染.pdf

    # 踏入C语言的奇妙编程世界 在编程的广阔宇宙中,C语言宛如一颗璀璨恒星,以其独特魅力与强大功能,始终占据着不可替代的地位。无论你是编程小白,还是有一定基础想进一步提升的开发者,C语言都值得深入探索。 C语言的高效性与可移植性令人瞩目。它能直接操控硬件,执行速度快,是系统软件、嵌入式开发的首选。同时,代码可在不同操作系统和硬件平台间轻松移植,极大节省开发成本。 学习C语言,能让你深入理解计算机底层原理,培养逻辑思维和问题解决能力。掌握C语言后,再学习其他编程语言也会事半功倍。 现在,让我们一起开启C语言学习之旅。这里有丰富教程、实用案例、详细代码解析,助你逐步掌握C语言核心知识和编程技巧。别再犹豫,加入我们,在C语言的海洋中尽情遨游,挖掘无限可能,为未来的编程之路打下坚实基础!

    性能对决:DeepSeek-V3与ChatGPTAPI在数学推理场景的基准测试.pdf

    在日常的工作和学习中,你是否常常为处理复杂的数据、生成高质量的文本或者进行精准的图像识别而烦恼?DeepSeek 或许就是你一直在寻找的解决方案!它以其高效、智能的特点,在各个行业都展现出了巨大的应用价值。然而,想要充分发挥 DeepSeek 的优势,掌握从入门到精通的知识和技能至关重要。本文将从实际应用的角度出发,为你详细介绍 DeepSeek 的基本原理、操作方法以及高级技巧。通过系统的学习,你将能够轻松地运用 DeepSeek 解决实际问题,提升工作效率和质量,让自己在职场和学术领域脱颖而出。现在,就让我们一起开启这场实用又高效的学习之旅吧!

    从零到一:手把手教你用Python调用DeepSeekAPI的完整指南.pdf

    在日常的工作和学习中,你是否常常为处理复杂的数据、生成高质量的文本或者进行精准的图像识别而烦恼?DeepSeek 或许就是你一直在寻找的解决方案!它以其高效、智能的特点,在各个行业都展现出了巨大的应用价值。然而,想要充分发挥 DeepSeek 的优势,掌握从入门到精通的知识和技能至关重要。本文将从实际应用的角度出发,为你详细介绍 DeepSeek 的基本原理、操作方法以及高级技巧。通过系统的学习,你将能够轻松地运用 DeepSeek 解决实际问题,提升工作效率和质量,让自己在职场和学术领域脱颖而出。现在,就让我们一起开启这场实用又高效的学习之旅吧!

Global site tag (gtag.js) - Google Analytics