Java关键字synchronized 使用中的 Double-Checked Locking is Broken
“Double-Checked Locking is Broken”声明
签名人: 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
Double-Checked Locking被广泛引用,并被用作在多线程环境中实现延迟初始化的有效方法。
不幸的是,在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之后,同步锁双重检查会尝试避免同步:
// 破坏多线程版本
// "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...}
不幸的是,该代码在优化编译器或共享内存多处理器的情况下不起作用。
它不起作用
有很多原因不起作用。我们要描述的第一个原因更明显。理解完这些之后,你可能会试图想出一种方法来“修复”双重检查的锁定习惯用法。您的修复程序无法正常工作:修复程序无法正常工作的原因更为细微。理解这些原因,提出更好的解决方案,但它仍然无法正常工作,因为还有更多细微的原因。
很多非常聪明的人花了很多时间看这个。有没有办法让它无需访问helper 对象进行同步每个线程工作。
第一个原因不起作用
最明显的原因是它不起作用,即初始化helper对象和写入helper字段的写入操作可以完成或完成顺讯不可知。因此,调用getHelper()的线程可以获得对helper对象的非空引用,但请参阅helper对象的字段的默认值,而不是在构造函数中设置的值。
如果编译器将调用内联到构造函数中,那么如果编译器可以证明构造函数不能抛出异常或执行同步,那么初始化对象和写入helper字段的写入可以自由重新排序。即使编译器不对这些写入重新排序,在多处理器上,处理器或内存系统可能会重新排列这些写入,如同在另一个处理器上运行的线程所感知的那样。
Doug Lea已经编写了基于编译器的重排序的更详细的描述。
一个测试用例表明它不起作用
Paul Jakubik发现了一个使用双重检查锁定的例子,它无法正常工作。这里有一个稍微清理过的代码版本。
在使用Symantec JIT的系统上运行时,它不起作用。特别是,Symantec JIT编译
singletons [i] .reference = new Singleton();
(注意,使用基于句柄的对象分配系统的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 lockhelper = h;} } return helper;}// other functions and members...}
此代码将Helper对象的构造放入内部同步块中。这里直观的想法是,在释放同步的位置应该存在内存屏障,并且应该防止对Helper对象的初始化进行重新排序,并且将分配给字段帮助器。
不幸的是,这种直觉是绝对错误的。同步规则不会那样工作。monitorexit的规则(即释放同步)是在监视器释放之前必须执行monitorexit之前的操作。然而,没有规定说monitorexit之后的操作可能不会在显示器发布之前完成。编译器移动赋值helper= h是完全合理和合法的; 在同步块内部,在这种情况下,我们又回到了之前的位置。许多处理器提供执行这种单向内存屏障的指令。更改语义以要求释放锁以成为完整的内存障碍将会导致性能损失。
更多修复程序无法使用
你可以做的事情是强迫作者执行完整的双向记忆障碍。这是严重的,效率低下的,而且几乎可以保证一旦Java内存模型被修改就不能工作。不要使用这个。为了科学的利益,我在一个单独的页面上描述了这种技术。不要使用它。
但是,即使初始化helper对象的线程执行完全内存屏障,它仍然不起作用。
问题在于,在某些系统上,为helper字段看到非空值的线程也需要执行内存屏障。
为什么?因为处理器拥有自己的本地高速缓存的内存副本。在某些处理器上,除非处理器执行缓存一致性指令(例如内存屏障),否则即使其他处理器使用内存屏障将其写入全局内存,也可以在本地缓存副本之外执行读取操作。
我已经创建了一个单独的网页,并讨论了如何在Alpha处理器上实际发生这种情况。
这是否值得麻烦?
对于大多数应用程序来说,简单地使getHelper() 方法同步的成本并不高。如果您知道这会对应用程序造成大量开销,那么您应该只考虑这种详细的优化。
通常,更高层次的聪明性,比如使用内置mergesort而不是处理交换排序(请参阅SPECJVM DB基准测试)将会产生更多影响。
使它适用于静态单例
如果你正在创建的单例是静态的(即只有一个Helper创建),而不是另一个对象的属性(例如,每个Foo对象都有一个Helper,那么就有一个简单而优雅的解决方案。
只需将单例定义为单独类中的静态字段即可。Java的语义保证字段不会被初始化,直到字段被引用,并且任何访问该字段的线程都将看到初始化该字段所产生的所有写入。
class Helper {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...}
事实上,假设compute Hash Code函数总是返回相同的结果并且没有副作用(即幂等),那么您甚至可以摆脱所有的同步。
// 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 显式内存障碍的C ++实现
// 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 checkTYPE* 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;}
使用线程本地存储解决双重检查锁定问题
Alexander Terekhov(TEREKHOV@de.ibm.com)提出了使用线程本地存储实现双重检查锁定的巧妙建议。每个线程保留一个线程本地标志来确定该线程是否完成了所需的同步。
class Foo {/** If perThreadInstance.get() returns a non-null value, this threadhas done synchronization needed to see initializationof 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 hereperThreadInstance.set(perThreadInstance);}}
这种技术的性能取决于你拥有的JDK实现。在Sun 1.2的实现中,ThreadLocal的速度很慢。它们在1.3中显着更快,并且预计在1.4中仍然更快。Doug Lea分析了一些实现延迟初始化的技术的性能。
在新的Java内存模型下
从JDK5开始,有一个新的Java内存模型和线程规范。
使用易失性固定双重锁定锁定
JDK5及更高版本扩展了易失性的语义,以便系统不会允许写入与先前的读取或写入相关的易失性数据,并且读取易失性数据不能针对后续读取或写入进行重新排序。请参阅 Jeremy Manson博客中的这个条目了解更多详情。
通过这种改变,可以通过声明helper字段是挥发性的,来使双重检查锁定成语工作。这在JDK4和更早版本下不起作用 。
// Works with acquire/release semantics for volatile
// Broken under current semantics for volatileclass Foo {private volatile Helper helper = null;public Helper getHelper() {if (helper == null) {synchronized(this) {if (helper == null)helper = new Helper();}}return helper;}}
双选锁定不可变对象
如果Helper是一个不可变的对象,这样Helper的所有字段都是最终的,那么双重检查锁定就可以工作,而不必使用易失性字段。这个想法是,对一个不可变对象(比如一个String或一个Integer)的引用应该和int或者float类似。读取和写入对不可变对象的引用是原子的。
双重检查成语的描述
- 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, TrvorMisfeldt, Jim Shur, Patrick Thompson, SIGS Reference library
- Global Variables in Java with the Singleton Pattern, Wiebe de Jong, Gamelan
Java关键字synchronized 使用中的 Double-Checked Locking is Broken相关推荐
- Java中的双重检查锁(double checked locking)
起因 在实现单例模式时,如果未考虑多线程的情况,很容易写出下面的代码(也不能说是错误的): public class Singleton {private static Singleton uniqu ...
- java 双重检查锁 有序_Java中的双重检查锁(double checked locking)
1 public classSingleton {2 private staticSingleton uniqueSingleton;3 4 privateSingleton() {5 }6 7 pu ...
- 双重检查锁Double Checked Locking Pattern的非原子操作下的危险性
Double Checked Locking Pattern 即双重检查锁模式. 双重检查锁模式是一种软件设计模式,用于减少获取锁的开销.程序首先检查锁定条件,并且仅当检查表明需要锁时才才获取锁. 延 ...
- 单例模式,懒汉饿汉,线程安全,double checked locking的问题
概览 本文目的 单例 饿汉模式 懒汉模式 线程安全的Singleton实现 懒汉普通加锁 double checked locking double checked locking 靠不住? 静态局部 ...
- 【java】java 关键字: synchronized详解
1.概述 转载:关键字: synchronized详解 [Java]Synchronized 有几种用法 [java] 从hotspot底层对象结构理解锁膨胀升级过程 [java]动态高并发时为什么推 ...
- Java关键字synchronized的简单理解
参考链接: https://blog.csdn.net/luoweifu/article/details/46613015 Java中并发编程使用中,最频繁和最简单的使用是synchronized关键 ...
- Java关键字synchronized详解
synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D ...
- 深入理解并发的关键字-synchronized
我们已经了解了Java内存模型的一些知识,并且已经知道出现线程安全的主要问题来源于JMM的设计,主要集中在主内存和线程的工作内存而导致的内存可见性问题,以及重排序导致的问题,进一步知道了happens ...
- 【Java】Synchronized解析以及多种用法
1.概述 [Java]Synchronized 有几种用法 [java] 从hotspot底层对象结构理解锁膨胀升级过程 [java]动态高并发时为什么推荐重入锁而不是Synchronized? [j ...
最新文章
- 在IT技术圈混,怎么能不知道这几个公众号
- 基于词典的逆向最大匹配中文分词算法,更好实现中英文数字混合分词
- CentOS7安装wdCP面板,快速搭建web运行环境(图文详解)
- CCNA第十一章学习笔记OSPF简介
- 【javascript】DOM操作方法(3)——document节点属性
- chrome jsp 显示不正常_JSP程序在chrome下不兼容的问题!
- 北美KubeCon新风,正把K8S魔力带向边缘计算
- adg oracle 架构_技术栈数据中心有了ADG架构就高枕无忧了?你还需要做这一步!...
- vmw6.5安装Freebsd8.1桌面gnome
- 一种结合实例和语义分割从田间图像中识别咖啡叶病虫害的深度学习方法
- Ubuntu系统备份和还原,从此避免系统重装
- 京东轮播图的原生代码
- 中兴机顶盒网关服务器超时,中兴机顶盒错误1302连接EPG服务失败解决方法
- 【HDL系列】半减器、全减器和减法器原理和设计
- Mac 在当前目录打开终端
- echarts 鼠标弹框显示百分比柱状图显示百分比
- 通过LL库初始化STM32的硬件IIC
- 鸢尾花数据集的线性多分类
- 犹豫许久还是在 CSDN(程序员之家) 开通了自己的第一个博客
- 周鸿伟鸿蒙系统,周鸿袆正式宣布!鸿蒙系统开源比较好,将全力支持华为新系统...
热门文章
- 云南贵州地区市场知名的调查研究咨询公司
- PHP microtime 返回当前 Unix 时间戳和微秒数
- 网站卡其cdn后不能访问_关于网站使用CDN后无法登录的解决办法
- 1259: [蓝桥杯2015初赛]三羊献瑞 C/C++
- 思杰虚拟服务器退出管理主机,思杰服务器虚拟化解决详尽方案介绍2012.ppt
- 新手上路:什么是API接口
- 微信公众号开发(3)-实现关键词自动回复
- 10198_基于SSM的电影票预订系统
- 小学数学加减法测试软件,小学数学加减乘除出题软件
- 瑞丰银行近日IPO过会,3年前曾被证监会取消审核