`

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

阅读更多

“双重检查锁定被打破”的声明
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等容器中用于确定...

    Amber16+分子模拟与计算化学的软件+生物分子(如蛋白质、核酸)的动态模拟+药物设计+膜蛋白研究及能量计

    Amber16 是一款在分子模拟与计算化学领域广泛应用的软件工具。它广泛应用于生物化学、药物设计、生物分子、生物大分子以及材料科学中的分子动力学模拟和相关计算研究。 用途 1. 生物分子模拟:模拟蛋白质、核酸、多糖等生物大分子的动态行为,研究其结构与功能的关系。 2. 药物设计与分子对接:分析小分子药物与生物靶标的结合模式,优化药物设计。 3. 膜蛋白模拟:利用 Lipid16 力场模拟磷脂双分子层,研究膜蛋白的结构与功能。 4. 能量计算与优化:进行能量最小化、自由能计算等,研究分子间的相互作用。 5. 轨迹分析:分析模拟轨迹,计算均方位移、RMSD、RMSF 等参数。 6. 力场转换与扩展:支持多种力场的转换和扩展,例如 CHARMM、AMOEBA。 技术关键词 - 分子动力学(MD):通过数值模拟研究分子在一定时间内的运动。 - 力场(Force Field):如 Amber 力场、Lipid14 力场,用于描述分子间的相互作用。 - GPU 加速:PMEMD 模块支持 GPU 加速,显著提高计算效率。

    59.基于51单片机的汽车倒车防撞报警系统(实物).pdf

    59.基于51单片机的汽车倒车防撞报警系统(实物).pdf

    计算机中~人工神经网络及其应用(导论).ppt 人工智能神经网络通过多样化架构(CNN、RNN、GAN等)与技术创新,已在医疗、

    人工智能神经网络及其应用主要包含以下六大核心要点: ‌一、基础概念与核心结构‌ 1. ‌定义与组成‌ 2. ‌工作原理‌ ‌二、常见神经网络架构‌ 3. ‌卷积神经网络(CNN)‌ 4. ‌循环神经网络(RNN) 5. ‌生成对抗网络(GAN) 6. ‌Transformer ‌三、关键技术组件‌ 7. ‌激活函数 8. ‌优化算法 9. ‌正则化技术 ‌四、核心应用领域‌ 10. ‌信息处理与模式识别 11. ‌医疗健康 12. ‌交通与工业 13. ‌金融与经济 14. ‌生成式应用 ‌五、发展趋势‌ 15. ‌算力与模型优化 16. ‌多模态融合 17. ‌轻量化与边缘计算 ‌六、挑战与伦理问题‌ 18. ‌数据依赖与可解释性 19. ‌安全与隐私 20. ‌伦理与监管

    10.基于51单片机的密码锁设计(仿真+实物).pdf

    10.基于51单片机的密码锁设计(仿真+实物)

    MySql导出表结构到Word文档 支持导出MySQL数据库表结构!! 运行环境:jdk8+,需要Java运行环境

    MySql导出表结构到Word文档 支持导出MySQL数据库表结构!! 运行环境:jdk8+,需要Java运行环境

    华为USG5500、USG5530系列升级固件v300r001c10spc600.bin

    华为USG5500、USG5530系列升级固件v300r001c10spc600.bin

    Delphi 12.3控件之手机秒变扫码枪,扫付款码收款Delphi FMX源代码多平台.rar

    Delphi 12.3控件之手机秒变扫码枪,扫付款码收款Delphi FMX源代码多平台.rar

    tuned-profiles-oracle-2.16.0-1.el8.x64-86.rpm.tar.gz

    1、文件说明: Centos8操作系统tuned-profiles-oracle-2.16.0-1.el8.rpm以及相关依赖,全打包为一个tar.gz压缩包 2、安装指令: #Step1、解压 tar -zxvf tuned-profiles-oracle-2.16.0-1.el8.tar.gz #Step2、进入解压后的目录,执行安装 sudo rpm -ivh *.rpm

    基于java的ssm小说阅读网站(含LW+PPT+源码+系统演示视频+安装说明).7z

    小说阅读网站,主要的模块包括查看;管理员;首页、个人中心、读者管理、作者管理、小说信息管理、小说分类管理、余额充值管理、购买小说管理、下载小说管理、系统管理,读者;个人中心、余额充值管理、购买小说管理、下载小说管理、我的收藏管理等,作者:个人中心、小说信息管理、小说分类管理、余额充值管理、购买小说管理、下载小说管理、我的收藏管理。首页:小说信息、我的、跳转到后台功能。系统中管理员主要是为了安全有效地存储和管理各类信息,还可以对系统进行管理与更新维护等操作,并且对前后台有相应的操作权限。 要想实现小说阅读网站的各项功能,需要后台数据库的大力支持。管理员验证注册信息,收集的读者信息,并由此分析得出的关联信息等大量的数据都由数据库管理。本文中数据库服务器端采用了Mysql作为后台数据库,使Web与数据库紧密联系起来。在设计过程中,充分保证了系统代码的良好可读性、实用性、易扩展性、通用性、便于后期维护、操作方便以及页面简洁等特点。 本系统的开发使获取小说阅读网站信息能够更加方便快捷,同时也使小说阅读网站信息变的更加系统化、有序化。系统界面较友好,易于操作。 关键词:小说阅读网站 ;jsp ;Mysql

    284.基于51单片机的风扇【自然风,手动,电位器,ADC0808】(仿真).pdf

    284.基于51单片机的风扇【自然风,手动,电位器,ADC0808】(仿真).pdf

    山东大学软件学院计算机网络实验

    山东大学软件学院计算机网络实验

    linux mipi-camera驱动程序 s5k33d-48

    linux mipi-camera驱动程序 s5k33d_48

    软件工程研究生课程:移动互联网技术及应用的教学大纲解析

    内容概要:本文档是重庆大学针对软件工程专业开设的一门《移动互联网技术及应用》的详细教学大纲。课程分为多个模块,涵盖了移动互联网的现状和技术基础、不同应用场景及其商业模式、案例分析和实践操作。课程还关注于手机网站开发、应用程序构建及特定功能如GPS定位的应用等方面的技术,旨在培养学生的理论素养和技术实现能力,最终能够独立完成一个移动互联网创新项目。评分依据为出勤和课堂表现的过程评价与作品的实际效果实践评价相结合的方式。 适合人群:即将就读或者正在研读移动互联网相关专业的高校研究生,尤其是已掌握Web开发基础并有意深入探究移动互联网技术方向的学生。 使用场景及目标:此课程非常适合那些计划未来投身于快速发展的移动互联行业的年轻人;它不仅可以加深他们对该行业最新趋势的理解,还可以锻炼实际解决问题的能力。 其他说明:教学材料包括一系列权威性的书籍作为参考资料,帮助学员更广泛地获取知识。此外,通过一系列有针对性的设计任务和小组合作的学习形式进一步提高学生的综合技能水平。

    眼动数据 - 副本.zip

    眼动数据 - 副本.zip

    20250323.pcapng

    20250323.pcapng

    2025年3月CCF编程能力认证(C++)五级.pdf

    2025年3月CCF编程能力认证(C++)五级.pdf

    多个网络摄像头进行拉流以及对象检测平台使用Jetson TX2 ARM architecture海康摄像头对象检测采用.zip

    yolo

Global site tag (gtag.js) - Google Analytics