本文根据http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html来翻译,纯粹为了自己学习做记录,有生硬不通的地方还请海涵,也欢迎各位朋友指正。
在多线程环境下实现延迟加载时 Double-Checked Locking是通常使用的而且效率比较高的方法。不幸的是,如果没有其他同步机制的话,他也许不能在java平台可靠的运行。当使用其他语言实现时,比如c++,这取决于处理器的内存模型,编译器引起的reordering 和编译器与synchronization 库之间的相互作用。因为这些不是针对特定的语言,比如c++,几乎可以说会在其中一种情况工作。显示的内存屏障(memory barriers)可以在c++中使用,但是不能用在java中。
首先解释期望的动作,思考下面的代码:
如果这个代码用于多线程环境,很多事都会出错。最明显的,两个或者更多Helper对象会被创建(我们后续会讲述其他问题)。简单解决这个问题是对getHelper()方法使用synchronize 关键字。
上面这段代码每次访问getHelper()方法都要同步进行,双重检查锁定(double-checked locking )试图避免在helpser对象创建后的同步化。
不幸的是,这个段代码将不能按期望工作在优化编译器或共享内存的多处理器。
他不能工作
不能工作有很多原因,下面描述的第一个原因很明显的。理解了这些,你也许会被诱惑着去设计修正双重检查锁定的方法。但是你的解决方案不能工作:有很多微妙的原因。理解这些原因之后想出更好的解决方案,但是这个也不能工作,因为还有更多微妙的原因。
很多聪明的人花了很多时间关注这个,但是除了让每个线程同步访问helpser对象外没有办法解决这个问题。
不能工作的第一个原因
不能工作的最明显原因是初始化Helper对象和helpser字段的赋值可能按顺序进行也可能颠倒。因此一个线程调用getHelper()能得到非空的helper对象引用,但是看到是helper里的字段是初始值,而不是构造函数里设置的值。
如果编译器内联到构造函数的调用且能保证构造函数不会抛出异常或执行同步。,那么初始化对象的操作和对helper 里字段的写入可以自由重排。
即使编译器没有重排这些操作,在一个多线程处理器或内存系统可能会重排这些写操作,当感知到另一个线程在另一个处理器上执行时。
Doug Lea写了一篇more detailed description of compiler-based reorderings.
测试用例,说明他不能工作
Paul Jakubik 发现一个使用双重检查锁定但是不能正确工作的例子.A slightly cleaned up version of that code is available here.
在使用Symantec JIT编译器的系统上不能工作。特别是Symantec JIT 编译
如下所示(注意Symantec JIT 使用基于句柄的对象分配系统)
就像你看到的,分配给singletons[i]的应用是在Singleton的构造函数被执行前。这在java的内存模型中是完全合法的,而且在c和c++中也同样。
一个不能工作的修正方案
根据上述的解释,一些人可能会给出下面的代码:
这段代码将Helper对象的构造函数放入synchronized 块内,直观的想法是在同步块释放的点有内存屏障(memory barrier ),这样能够避免颠倒初始化Helper和对helper字段赋值的顺序。
不幸的是,这个是完全错误的。这个同步规则将不会这么工作。对于监视器退出规则(比如:释放同步)是指在监视退出前的操作必须在监视器释放前执行。然而没有规则说那些在监视器退出之后的操作不能在监视器释放前完成。编译器将变量的分配helper = h放入同步块是完全合法的,在这种情况下我们又回到了之前的地方。许多处理器提供执行这种单向内存屏障的指令。但是变更语义要求释放锁是一个完整的内存屏障将有性能损失。
更多的不能工作的修补程序
有些操作可以强制写进程执行完全的双向内存屏障。但这是粗劣,低效的,而且一旦java内存模型改变基本上就不能工作。不要使用这些技术。
但是即使在helper对像初始化时,一个完整的内存屏障被线程执行,他仍然无法正常工作。
问题是在某些系统中,那些看到helper字段非空值的线程也需要执行内存屏障。
为什么?因为处理器有他们自己本地的内存副本。某些处理器,如果没有执行缓存一致性指令(例如,内存屏障),读线程可能读到过期的本地缓存副本,即使其他处理器使用内存屏障强制写进程写到主内存。这里专门讨论这种情况怎样发生在Alpha 处理器上 a separate web page。
是否值得这么麻烦?
对大多数应用来说,简单的将getHelper()方法同步化的成本并不高。只有在你知道这会造成应用程序重大开销而且可以接受的情况下才考虑这种优化方案。
很多时候,更聪明的方法是使用内建的归并排序而不是处理交换排序(见 JVM DB基准说明)会更有效。
在静态单例的情况下工作
如果你建立的单例是静态的(比如,只有一个Helper对象会被创建),相对于另一种对象属性(比如,每一个Foo对象有一个Helper对象)有一种简单优雅的解决方法。
只要在一个单独的类中定义一个静态字段。Java的语义保证字段不会被初始化直到字段被引用,而且所有使用该字段的线程将看到所有在初始化字段时的写入结果。
在32位原始值上工作
虽然双重检查锁定不能用于引用类型对象,但是他可以用于32位原始类型(比如,int或float)。注意他不能工作于long或者double,因为非同步的64位原始类型的读写不保证是原子的。
事实上假设方法computeHashCode()总是返回相同的结果而且没有副作用(比如,幂等--一个操作不会修改状态信息,并且每次操作的时候都返回同样的结果。即:做多次和做一次的效果是一样 的。)你甚至可以去掉所有的同步。
使用显示的内存屏障
如果你有明确的内存屏障指令他可能会让双重检查锁定模式工作。比如,如果你使用c++,你可以使用Doug Schmidt 等人书中的代码:
使用ThreadLocal 存储
Alexander Terekhov 提出了一个聪明的建议:使用thread local存储来实现双重检查锁定。每个线程保留一个线程本地标志来确定该线程是否已完成所需的同步。
这种技术的性能相当程度上取决于你使用的JDK,在JDK1.2上很慢,但是在JDK1.3之后的版本就快很多。
新的java内存模型
JDK5,a new Java Memory Model and Thread specification.
是用Volatile关键字
JDK5及以后的版本扩展了volatile的语义,这样系统将不允许对一个volatile的写操作与之前的读写操作进行重排。而且对volatile的读操作也不会与后续的读写进行重排。详细请见: this entry in Jeremy Manson's blog
根据这个双重检查锁定可以在helper字段声明为volatile时正常工作,但是在JDK4及之前不行。
双重检查锁定不变对象
如果Helper是不可变对象,比如Helper所有字段都是final,那么不是用volatile关键字双重检查锁定也能正常工作。这个思路是一个不可变对象(比如,String 或 Integer)在很大程度上就像int或float;读写不可变对象是原子的。
分享到:
相关推荐
C++ and the Perils of Double-Checked Locking 关于单例模式C++实现的一些问题
C++ and the Perils of Double Checked Locking.zip
• Chapter 21: The Singleton Pattern and the Double-Checked Locking Pattern • Chapter 22: The Object Pool Pattern • Chapter 23: The Factory Method Pattern • Chapter 24: Summary of Factories (no ...
在做角色权限,通过tree控件展示权限列表的时候,用:default-checked-keys绑定数据,这个属性不能实现双向绑定,只能用于初始化选择状态。如果想双向绑定只能使用组件的方法setCurrentKey来实现。想直接绑定数据来的...
C++ 怎么解决 单例模式的线程安全问题
这个小程序涉及到了以下知识点: Java基础知识 队列《数据结构》 单例模式“双检锁/双重校验锁(DCL,即 double-checked locking)”
本文主要讲解AngularJS ng-checked 指令,在这整理ng-checked 指令的基础知识,并附代码实例,有需要的小伙伴可以参考下
Software Adaptation in an Open Environment: A Software Architecture Perspective by Yu Zhou ...The organization and presentation of the book will be double-checked by professional scholars
北京火龙果软件工程技术中心意图无论什么时候当临界区中的代码仅仅需要加锁一次,同时当其获取锁的时候必须是线程安全的,可以用DoubleCheckedLocking模式来减少竞争和加锁载荷。动机1、标准的单例。开发正确的有效...
本篇文章主要介绍了深究AngularJS——ng-checked(回写:带真实案例代码),具有一定的参考价值,感兴趣的小伙伴们可以参考一下
四色定理的计算机证明,网上有coq平台和定理证明的源代码下载。
双重检测锁(Double-Checked Locking)实现的Singleton模式在多线程应用中有相当的价值。在ACE的实现中就大量使用ACE_Singleton模板类将普通类转换成具有Singleton行为的类。这种方式很好地消除了一些重复代码臭味,...
checked.css是一款能够制作复选框和单选按钮点击动画的CSS3动画库。它内置23种动画特效,在用户点击单选按钮或复选框时会出现相应的动画效果。
各种工厂模式 242 第21章 Singleton模式和Double-Checked Locking模式 249 第22章 Object Pool模式 257 第23章 Factory Method模式 267 第24章 工厂模式的总结 272 第八部分 终点与起点 第25章 设计模式回顾:总结与...
多检查奇迹:checked伪类 polyfill(适用于 IE8 及以下)。如何使用包括 jQuery 1.x(与旧 IE 兼容)>= 1.7,然后在 IE 条件注释中包含 poly-checked 脚本: < script src =" //ajax.googleapis....
北京交通大学期末试卷汇编作业-checked.docx
AngularJS是当前非常的流行的前端框架,它的语法糖非常多,也极大的方便了前端开发者。这篇文章主要介绍了ng-options和ng-checked在表单中的高级运用,需要的朋友可以参考下
代码片段: function isAllChecked(){ ... allChecked = $(this).find(":checked").not("[name='all-checked']") , thisAllCheckBtn = $(this).find(".all-checked [name='all-checked']");
1.寂寞的Singleton 2. 当Singleton遇见多线程 4.安全发布 6. 讨论的延续 1. JavaWorld章:Double-checked l