`
nanjingjiangbiao_T
  • 浏览: 2688407 次
  • 来自: 深圳
文章分类
社区版块
存档分类
最新评论

关于双重锁

 
阅读更多

Double-Checked Locking( 双检锁 ) 是普遍应用的技术,尤其在多线程环境下是实现延迟加载的有效方法。

然而,在其 Java 实现中,如果不做同步控制它不能保证在任何平台总能正确的执行。而在其他语言实现中如: C++ ,双检锁能否正确执行取决于处理器的内存模型、编译器的对指令的乱序优化以及编译器与同步库之间的相互影响。因为上面三种因素在诸如 C++ 等编程语言中并没有明确的规范,因此,很难明确的说明在什么情形下双检锁能正确执行。在 C++ 中显式的内存障栅 (Memory Barrier 解释见 : http://en.wikipedia.org/wiki/Memory_barrier) 能保证双检锁正确执行,但在 Java 中并没有此类内存障栅。

为了解释程序执行的预期行为,看下面的代码 :

  1. //Singlethreadedversion
  2. classFoo{
  3. privateHelperhelper=null;
  4. publicHelpergetHelper(){
  5. if(helper==null)
  6. helper=newHelper();
  7. returnhelper;
  8. }
  9. //otherfunctionsandmembers...
  10. }

如果这段代码运行在多线程环境,存在很多导致运行出错的情形。最显而易见的是 : 可能在 Foo 中分配不止一个 Helper 对象 ( 还有其他问题待会下面再解释 ) ,要修复这个问题只需给 getHelper 方法加上同步:

  1. //Correctmultithreadedversion
  2. classFoo{
  3. privateHelperhelper=null;
  4. publicsynchronizedHelpergetHelper(){
  5. if(helper==null)
  6. helper=newHelper();
  7. returnhelper;
  8. }
  9. //otherfunctionsandmembers...
  10. }

上面的代码中,每次调用 getHelper() 方法都会执行同步操作,而双检锁则是避免每次调用都进行同步的习惯用法(只有 helper 对象在第一次构造的时候需要同步)。

  1. //Brokenmultithreadedversion
  2. //"Double-CheckedLocking"idiom
  3. classFoo{
  4. privateHelperhelper=null;
  5. publicHelpergetHelper(){
  6. if(helper==null)
  7. synchronized(this){
  8. if(helper==null)
  9. helper=newHelper();
  10. }
  11. returnhelper;
  12. }
  13. //otherfunctionsandmembers...
  14. }


不幸的是,以上代码在当前优化编译器或共享内存式的多处理器中不能正常工作。

有很多原因导致代码执行失败,我们先描述一些比较明显的出错情形,等明白这些后你可能会尝试修复双检锁的惯用法。但是你的方法很可能不会奏效:因为其中还有很多微妙的原因。明白这些后,你想出一个更好的修复方法,但是仍旧不会奏效,因为还有更多秘密蕴含其中。

很多聪明的人花费了大量时间研究这个,看有没有办法在保证执行正确的情况下,无需每个访问 helper 对象的线程都进行同步。

1. 最明显的导致执行失败的情形是:当初始化 Helper 对象时,对 helper 实例赋值这一操作可能已经完成也可能没有。因此,当一个线程调用 getHelper() 可能看到一个非空的 helper 引用,但此时 Helper 对象并没有完成全部的初始化工作,线程这时看到的 helper 对象里面的值都是默认值,而不是 Helper() 构造函数里面设置的值。

如果编译器将构造函数的调用内联化,那么 Helper 对象的初始化和对 helper 实例赋值的指令可以随意重新排序,只要编译器能够确保构造器不会抛出异常或执行同步。即使编译器不重新排序指令,在多处理器系统中,当某个线程运行在一个处理器上,另外的处理器或内存系统可能打乱写操作顺序。 ( 具体细节见:more detailed description of compiler-based reorderings)

一个测试失败执行的案例:

Paul Jakubik 找到一个使用双检锁无法正常执行的例子。当运行的系统使用 SymantecJIT ,代码不能正常工作。特别的, SymantecJIT 编译: singletons[i].reference = new Singleton(); 时产生的机器码如下:

  1. 0206106Amoveax,0F97E78h
  2. 0206106Fcall01F6B210;为Singleton分配空间,返回值到eax
  3. 02061074movdwordptr[ebp],eax;EBP是&singletons[i]的引用,未构造好的对象存储在这里
  4. 02061077movecx,dwordptr[eax];取消引用句柄获取原始指针
  5. 02061079movdwordptr[ecx],100h;接下来的4行是Singleton的内联构造函数
  6. 0206107Fmovdwordptr[ecx+4],200h
  7. 02061086movdwordptr[ecx+8],400h
  8. 0206108Dmovdwordptr[ecx+0Ch],0F84030h

正如上面演示的,对 singletons[i].reference 赋值的操作,是在构造器被调用前执行的。而这在现有的JMM 下是完全合法的,C/C++ 同样也合法( 它们都没有指定内存模型)

无法正常工作的修复方式:

通过上面的解释,有人提出如下的解决方法;

  1. //(Still)Brokenmultithreadedversion
  2. //"Double-CheckedLocking"idiom
  3. classFoo{
  4. privateHelperhelper=null;
  5. publicHelpergetHelper(){
  6. if(helper==null){
  7. Helperh;
  8. synchronized(this){
  9. h=helper;
  10. if(h==null)
  11. synchronized(this){
  12. h=newHelper();
  13. }//releaseinnersynchronizationlock
  14. helper=h;
  15. }
  16. }
  17. returnhelper;
  18. }
  19. //otherfunctionsandmembers...
  20. }

这段代码把Helper 对象的构造放进了内部同步块中,这里的想法是在同步锁释放的地方需要一个Memory Barrier 以阻止Helper 对象的初始化操作和对helper 实例赋值操作之间的乱序。

但是,这个想法是错误的,同步不是按照那个规则执行的。Monitorexit 执行的规则是在monitorexit 前面的动作,必须在monitor 释放前执行。然而,并没有规则说明monitorexit 之后的动作不会在monitorexit 释放前执行。编译器将赋值语句:helper=h ;放进里面的同步块中是合理的,这样又回到了前面我们介绍的情形。许多处理器提供了这种执行单路memory barrier 的指令,因为像上面这种以变更语义的方法,通过释放锁来获取全局的Memory Barrier 的方式是由性能上的损耗的。

更多无法凑效的修复方法

也许你想强制让writer 执行一个全局、双向的memory barrier ,但这种方式臃肿,低效而且一旦JMM 进行修订就不能保证能正确工作了。然而,即使线程在初始化helper 对象时使用全局的memory barrier ,仍不能达到预期的目标。在有些系统中,线程如果要看到一个非空的helper 实例,也需要执行memory barrier 。因为处理器中有对内存数据的本地 cache ,一些处理器,除非执行一条cache coherence 指令如memory barrier ,否则即使其他处理器使用memory barrier 把结果写入全局内存中,处理器执行的还是本地cache 保存的过时的数据。( 这里说明了需要使用 volatile 变量 的理由) 。

这是一个讨论在Aopha 处理器发生这种情形的页面:http://www.cs.umd.edu/~pugh/java/memoryModel/AlphaReordering.html

是否值得这么麻烦去做:

对大多数应用,执行getHeler 方法进行同步的代价并不会很大。应用中当这种同步对性能造成负担时有必要考虑使用其他的方法。比如:不使用交换排序而使用Java 内置的归并排序时同步会有较大的性能影响。

使用静态单例:

如果你创建的是静态的单实例,那么会有更好的解决方法。只需在单独的类中定义一个静态实例,Java 的语义会保证实例只有在主动使用时被初始化,且任何线程看到的总是完整的初始化结果。

  1. classHelperSingleton{
  2. staticHelpersingleton=newHelper();
  3. }

32 位原始数据类型能正常工作

尽管双检锁惯用法不适用于对象引用,但是对32 位的原始数据类型是可以正常工作的。需要注意的是long 和double 类型也是不能正常工作的,因为64 位原始数据类型的读写操作,如果不同步的话也是不能保证原子性的。

  1. //CorrectDouble-CheckedLockingfor32-bitprimitives
  2. classFoo{
  3. privateintcachedHashCode=0;
  4. publicinthashCode(){
  5. inth=cachedHashCode;
  6. if(h==0)
  7. synchronized(this){
  8. if(cachedHashCode!=0)returncachedHashCode;
  9. h=computeHashCode();
  10. cachedHashCode=h;
  11. }
  12. returnh;
  13. }
  14. //otherfunctionsandmembers...
  15. }

跟int或float基本是相同的,他们能保证原子性。

实际上,假设computeHashCode 函数总是返回相同的结果,而且没有副作用( 即幂等的) ,此时你可以完全不用同步操作。(h 是32int 型,所以赋值是原子的,而函数调用也是幂等因此不会存在不一致的中间结果)

  1. //Lazyinitialization32-bitprimitives
  2. //Thread-safeifcomputeHashCodeisidempotent
  3. classFoo{
  4. privateintcachedHashCode=0;
  5. publicinthashCode(){
  6. inth=cachedHashCode;
  7. if(h==0){
  8. h=computeHashCode();
  9. cachedHashCode=h;
  10. }
  11. returnh;
  12. }
  13. //otherfunctionsandmembers...
  14. }

使用显式的memory barriers保证正确执行
如果提供了显式的memory barriers指令,就能够保证双检锁模式正确执行。例如:如果使用C++,你可以使用Doug Schmidt书中的代码:


  1. //C++implementationwithexplicitmemorybarriers
  2. //Shouldworkonanyplatform,includingDECAlphas
  3. //From"PatternsforConcurrentandDistributedObjects",
  4. //byDougSchmidt
  5. template<classTYPE,classLOCK>TYPE*
  6. Singleton<TYPE,LOCK>::instance(void){
  7. //Firstcheck
  8. TYPE*tmp=instance_;
  9. //InserttheCPU-specificmemorybarrierinstruction
  10. //tosynchronizethecachelinesonmulti-processor.
  11. asm("memoryBarrier");
  12. if(tmp==0){
  13. //Ensureserialization(guard
  14. //constructoracquireslock_).
  15. Guard<LOCK>guard(lock_);
  16. //Doublecheck.
  17. tmp=instance_;
  18. if(tmp==0){
  19. tmp=newTYPE;
  20. //InserttheCPU-specificmemorybarrierinstruction
  21. //tosynchronizethecachelinesonmulti-processor.
  22. asm("memoryBarrier");
  23. instance_=tmp;
  24. }
  25. returntmp;
  26. }

使用TheadLocal修复双检锁
Alexander Terekhov (TEREKHOV@de.ibm.com) 提出一个巧妙的方法即通过线程局部存储实现双检锁。每一个线程都保持有一个局部标志来判断显示是否完成所需的同步操作。
  1. classFoo{
  2. /**IfperThreadInstance.get()returnsanon-nullvalue,thisthread
  3. hasdonesynchronizationneededtoseeinitialization
  4. ofhelper*/
  5. privatefinalThreadLocalperThreadInstance=newThreadLocal();
  6. privateHelperhelper=null;
  7. publicHelpergetHelper(){
  8. if(perThreadInstance.get()==null)createHelper();
  9. returnhelper;
  10. }
  11. privatefinalvoidcreateHelper(){
  12. synchronized(this){
  13. if(helper==null)
  14. helper=newHelper();
  15. }
  16. //Anynon-nullvaluewoulddoastheargumenthere
  17. perThreadInstance.set(perThreadInstance);
  18. }
  19. }

这种实现方法的性能开销不同的JDK实现有较大的差别。在SUN JDK1.2中,ThreadLocal执行很慢,但1.3快了很多,1.4更快了。
Doug Lea analyzed the performance of some techniques for implementing lazy initialization .

在新的JMM下:
JDK5,提出了新的JMM和线程规范 a new Java Memory Model and Thread specification .。
使用Volatile修复双检锁:
JDK5及以后的版本扩展了原有Volatile的语义以确保系统对一个Volatile变量的写操作跟前面的读写操作不会被重新排序,读操作也如此。更多的细节见: this entry in Jeremy Manson's blog 
这样,在双检锁中声明helper变量为Volatile即可,这种方式在JDK1.4及以前不能正常工作:
  1. //Workswithacquire/releasesemanticsforvolatile
  2. //Brokenundercurrentsemanticsforvolatile
  3. classFoo{
  4. privatevolatileHelperhelper=null;
  5. publicHelpergetHelper(){
  6. if(helper==null){
  7. synchronized(this){
  8. if(helper==null)
  9. helper=newHelper();
  10. }
  11. }
  12. returnhelper;
  13. }
  14. }

不可变对象的双检锁:
如果Helper对象是不可变的,比如:Helper对象的所有实例变量都是final,此时双检锁不需使用volatile关键词。其主要原理是对不可变对象如:String,Integer的引用的读写操作跟基本的32位数据类型一样,都能保证原子性。

Descriptions of double-check idiom

分享到:
评论

相关推荐

    星辉双重震动锁机源码

    在IT行业中,"星辉双重震动锁机源码"是一个涉及到设备安全和用户认证的专题。这个主题的核心是通过特定的震动模式来实现设备的锁定和解锁功能,为用户提供一种新颖且安全的交互方式。下面将详细介绍这个技术知识点:...

    双重检查锁

    ### 双重检查锁——新旧内存模型的对比 #### 一、双重检查锁概念解析 双重检查锁(Double-Checked Locking, DCL)是一种在多线程环境中用于实现懒加载(lazy loading)的设计模式。它通过两次检查来确定是否需要获取锁...

    电子功用-双重锁合电梯厅门联锁

    在这个特定的案例中,我们关注的是“双重锁合电梯厅门联锁”,这是一种增强安全性、提高可靠性的设计策略。 双重锁合机制意味着电梯厅门的闭合状态需要通过两个独立的感应器或开关来确认,这两个开关通常分布在门的...

    双重锁及请勿打扰的处理标准与程序.pdf

    双重锁及请勿打扰的处理标准与程序.pdf

    电子政务-双重电动锁风阀.zip

    在标题提到的“双重电动锁风阀”中,我们可以理解为这是电子政务系统中的一个特定组成部分,可能是用于保障数据安全或者系统稳定性的关键设备。它可能涉及到数据加密、权限控制和网络安全等多个方面。 首先,我们要...

    行业资料-电子功用-具有双重锁紧功能的充电弓铜排、受电弓铜排以及设备的介绍分析.rar

    本文将深入探讨具有双重锁紧功能的充电弓铜排和受电弓铜排,以及相关的设备技术。 首先,充电弓铜排是一种用于电力接收的导电装置,它安装在电动车或有轨电车顶部,与架空接触网进行接触,从而为车辆提供动力电源。...

    行业文档-设计装置-一种定时识别的双重开锁保险柜.zip

    行业文档-设计装置-一种定时识别的双重开锁保险柜.zip

    电信设备-一种双重防盗通讯机柜锁.zip

    文件名为"一种双重防盗通讯机柜锁.pdf",我们可以推测这是一份详细的技术文档,描述了这种双重防盗通讯机柜锁的设计原理、功能特点以及安装与使用方法。 首先,双重防盗机制通常指的是采用两种或以上的安全措施,以...

    xfhy#Android-Notes#7.单例模式的双重检查锁为什么必须加volatile1

    这种写法可以保证线程安全.两个if都是不能去掉的.如果去掉第一个if: 那么所有的线程都会到这里来先获取锁,然后判断singleton是否为空.所有线程都会串行

    单例双检锁

    在努力创建更有效的代码时,Java 程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量。然而,由于一些不太常见的 Java 内存模型细节的原因,并不能保证这个双重检查锁定习语有效。 ...

    基于双重指纹的无线智能门锁系统设计.pdf

    本文主要探讨了一种基于双重指纹技术的无线智能门锁系统设计,旨在提高门锁的安全性能并降低功耗。系统设计采用STM32F103C8T6微控制器作为门锁控制器,因其低成本、低功耗和高稳定性。ESP8266 WIFI串口模块被选为...

    指纹密码锁_C51_c51门锁_c51智能锁背景_指纹门锁_指纹锁_

    本文将深入探讨一款基于C51微控制器的智能门锁设计,该门锁融合了指纹识别和数字密码双重解锁方式,为用户提供更高级别的安全保障。 C51是美国Atmel公司生产的8位微控制器,广泛应用于嵌入式系统设计。它的特点是...

    基于双重指纹的无线智能门锁系统设计 (1).pdf

    《基于双重指纹的无线智能门锁系统设计》这篇论文主要探讨了一种创新的智能门锁系统,旨在解决传统智能门锁存在的问题,如使用距离有限、指纹解锁的安全性以及高能耗等。以下是该系统设计的关键知识点: 1. **双重...

    java实现单例模式-双重校验锁模式(线程安全)

    双重校验锁模式结合了懒汉模式和饿汉模式的优点,既实现了延迟加载,又保证了线程安全。你可以根据需求选择合适的单例模式实现方式。

    双重联锁正反转控制电路图.pdf

    双重联锁正反转控制电路图 一、双重联锁正反转控制电路图概述 双重联锁正反转控制电路图是一种复杂的控制电路图,用于控制电动机的正反转运转。该电路图由多个组件组成,包括双重联锁触头、常开触头、常闭触头、...

    电子政务-兆瓦级风力发电机组具有双重锁定功能的主轴锁.zip

    "兆瓦级风力发电机组具有双重锁定功能的主轴锁"是一个专门针对这一问题的技术解决方案,旨在确保设备在维护、故障排查或极端天气条件下的稳定与安全。主轴锁作为风力发电机的核心组件之一,其设计和功能对于整个系统...

    安全风险分级管控和隐患排查治理双重预防机制管理制度.doc

    安全风险分级管控和隐患排查治理双重预防机制管理制度 安全风险分级管控和隐患排查治理双重预防机制管理制度是为了强化安全展开理念,翻新安全管理方式,增强安全消费任务,无效停止事故发作,保证员工生命财富安全...

    双重互锁正反转实物图

    双重互锁正反转控制电路图 如下图所示,图中SB2和SB3均为复合按钮,合上电源开关Q,按下起动按钮SB2,其常闭触点SB2断开,使接触器KM2不得电;常开触点SB2接通,使接触器KM1得电吸合并自锁,其主触点闭合,接通电源...

    电子政务-双重防破解无线遥控电子密码锁.zip

    电子政务-双重防破解无线遥控电子密码锁.zip

    单例模式与双重检测

    双重检测机制的核心在于"双检锁",即两次检查实例是否已经创建。代码示例如下: ```java public class Singleton { private volatile static Singleton instance; private Singleton() {} public static ...

Global site tag (gtag.js) - Google Analytics