The Famous Double-Check Locking Technique
Java
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
Double-Checked Locking is widely cited and used as an efficient method for implementing lazy initialization in a multithreaded environment.
Unfortunately, it will not work reliably in a platform independent way when implemented in Java, without additional synchronization. When implemented in other languages, such as C++, it depends on the memory model of the processor, the reorderings performed by the compiler and the interaction between the compiler and the synchronization library. Since none of these are specified in a language such as C++, little can be said about the situations in which it will work. Explicit memory barriers can be used to make it work in C++, but these barriers are not available in Java.
To first explain the desired behavior, consider the following code:
// Single threaded version class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) helper = new Helper(); return helper; } // other functions and members... } |
If this code was used in a multithreaded context, many things could go wrong. Most obviously, two or more Helper objects could be allocated. (We'll bring up other problems later). The fix to this is simply to synchronize the getHelper() method:
// 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... } |
The code above performs synchronization every time getHelper() is called. The double-checked locking idiom tries to avoid synchronization after the helper is allocated:
// 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... } |
Unfortunately, that code just does not work in the presence of either optimizing compilers or shared memory multiprocessors.
It doesn't work
There are lots of reasons it doesn't work. The first couple of reasons we'll describe are more obvious. After understanding those, you may be tempted to try to devise a way to "fix" the double-checked locking idiom. Your fixes will not work: there are more subtle reasons why your fix won't work. Understand those reasons, come up with a better fix, and it still won't work, because there are even more subtle reasons.
Lots of very smart people have spent lots of time looking at this. There is no way to make it work without requiring each thread that accesses the helper object to perform synchronization.
The first reason it doesn't work
The most obvious reason it doesn't work it that the writes that initialize the Helper object and the write to the helper field can be done or perceived out of order. Thus, a thread which invokes getHelper() could see a non-null reference to a helper object, but see the default values for fields of the helper object, rather than the values set in the constructor.
If the compiler inlines the call to the constructor, then the writes that initialize the object and the write to the helper field can be freely reordered if the compiler can prove that the constructor cannot throw an exception or perform synchronization.
Even if the compiler does not reorder those writes, on a multiprocessor the processor or the memory system may reorder those writes, as perceived by a thread running on another processor.
Doug Lea has written a more detailed description of compiler-based reorderings.
A test case showing that it doesn't work
Paul Jakubik found an example of a use of double-checked locking that did not work correctly. A slightly cleaned up version of that code is available here.
When run on a system using the Symantec JIT, it doesn't work. In particular, the Symantec JIT compiles
singletons[i].reference = new Singleton();
to the following (note that the Symantec JIT using a handle-based object allocation system).
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
As you can see, the assignment to singletons[i].reference is performed before the constructor for Singleton is called. This is completely legal under the existing Java memory model, and also legal in C and C++ (since neither of them have a memory model).
A fix that doesn't work
Given the explanation above, a number of people have suggested the following code:
// (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... } |
This code puts construction of the Helper object inside an inner synchronized block. The intuitive idea here is that there should be a memory barrier at the point where synchronization is released, and that should prevent the reordering of the initialization of the Helper object and the assignment to the field helper.
Unfortunately, that intuition is absolutely wrong. The rules for synchronization don't work that way. The rule for a monitorexit (i.e., releasing synchronization) is that actions before the monitorexit must be performed before the monitor is released. However, there is no rule which says that actions after the monitorexit may not be done before the monitor is released. It is perfectly reasonable and legal for the compiler to move the assignment helper = h; inside the synchronized block, in which case we are back where we were previously. Many processors offer instructions that perform this kind of one-way memory barrier. Changing the semantics to require releasing a lock to be a full memory barrier would have performance penalties.
More fixes that don't work
There is something you can do to force the writer to perform a full bidirectional memory barrier. This is gross, inefficient, and is almost guaranteed not to work once the Java Memory Model is revised. Do not use this. In the interests of science, I've put a description of this technique on a separate page. Do not use it.
However, even with a full memory barrier being performed by the thread that initializes the helper object, it still doesn't work.
The problem is that on some systems, the thread which sees a non-null value for the helper field also needs to perform memory barriers.
Why? Because processors have their own locally cached copies of memory. On some processors, unless the processor performs a cache coherence instruction (e.g., a memory barrier), reads can be performed out of stale locally cached copies, even if other processors used memory barriers to force their writes into global memory.
I've created a separate web page with a discussion of how this can actually happen on an Alpha processor.
Is it worth the trouble?
For most applications, the cost of simply making the getHelper() method synchronized is not high. You should only consider this kind of detailed optimizations if you know that it is causing a substantial overhead for an application.
Very often, more high level cleverness, such as using the builtin mergesort rather than handling exchange sort (see the SPECJVM DB benchmark) will have much more impact.
Making it work for static singletons
If the singleton you are creating is static (i.e., there will only be one Helper created), as opposed to a property of another object (e.g., there will be one Helper for each Foo object, there is a simple and elegant solution.
Just define the singleton as a static field in a separate class. The semantics of Java guarantee that the field will not be initialized until the field is referenced, and that any thread which accesses the field will see all of the writes resulting from initializing that field.
class HelperSingleton { static Helper singleton = new Helper(); } |
It will work for 32-bit primitive values
Although the double-checked locking idiom cannot be used for references to objects, it can work for 32-bit primitive values (e.g., int's or float's). Note that it does not work for long's or double's, since unsynchronized reads/writes of 64-bit primitives are not guaranteed to be atomic.
// 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... } |
In fact, assuming that the computeHashCode function always returned the same result and had no side effects (i.e., idempotent), you could even get rid of all of the synchronization.
// 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... } |
Making it work with explicit memory barriers
It is possible to make the double checked locking pattern work if you have explicit memory barrier instructions. For example, if you are programming in C++, you can use the code from Doug Schmidt et al.'s book:
// 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; } |
Fixing Double-Checked Locking using Thread Local Storage
Alexander Terekhov (TEREKHOV@de.ibm.com) came up clever suggestion for implementing double checked locking using thread local storage. Each thread keeps a thread local flag to determine whether that thread has done the required synchronization.
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); } } |
The performance of this technique depends quite a bit on which JDK implementation you have. In Sun's 1.2 implementation, ThreadLocal's were very slow. They are significantly faster in 1.3, and are expected to be faster still in 1.4. Doug Lea analyzed the performance of some techniques for implementing lazy initialization.
Under the new Java Memory Model
As of JDK5, there is a new Java Memory Model and Thread specification.
Fixing Double-Checked Locking using Volatile
JDK5 and later extends the semantics for volatile so that the system will not allow a write of a volatile to be reordered with respect to any previous read or write, and a read of a volatile cannot be reordered with respect to any following read or write. See this entry in Jeremy Manson's blog for more details.
With this change, the Double-Checked Locking idiom can be made to work by declaring the helper field to be volatile. This does not work under JDK4 and earlier.
// 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; } } |
Double-Checked Locking Immutable Objects
If Helper is an immutable object, such that all of the fields of Helper are final, then double-checked locking will work without having to use volatile fields. The idea is that a reference to an immutable object (such as a String or an Integer) should behave in much the same way as an int or float; reading and writing references to immutable objects are atomic.
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

版权声明:本文为博主原创文章,未经博主允许不得转载。
JAVA 单例双重检查(double check)为什么不好用
在阅读之前,请先了解下线程并发涉及到的三个概念:原子性、可见性、有序性,可以看下这篇文章:http://www.cnblogs.com/dolphin0520/p/3920373.html
我假设你已经看过几篇double check的文章,但还是一知半解。
我们先看这种双重检查,不加volatile
public static Singleton instance;
public static Singleton getInstance()
{
if (instance == null) //1
{ //2
synchronized(Singleton.class) { //3
if (instance == null) //4
instance = new Singleton(); //5
}
}
return instance;
}
这种方式存在什么问题呢?
也许有人说存在可见性问题:线程1执行完第5步,释放锁。线程2获得锁后执行到第4步,由于可见性的原因,发现instance还是null,从而初始化了两次。
但是不会存在这种情况,因为synchronized能保证线程1在释放锁之前会讲对变量的修改刷新到主存当中,线程2拿到的值是最新的。
实际存在的问题是无序性。
第5步这个new操作是无序的,它可能会被编译成:
- a. 先分配内存,让instance指向这块内存
- b. 在内存中创建对象
然而我们需要意识到这么一个问题,synchronized虽然是互斥的,但不代表一次就把整个过程执行完,它在中间是可能释放时间片的,时间片不是锁。(我因为这里没转过来,耽误了很多时间)
也就是说可能在a执行完后,时间片被释放,线程2执行到1,这时他读到的instance是不是null呢?(标记1)
基于可见性,可能是null,也可能不是null。
非常奇葩的是,在这个例子中,如果读到的是null,反而没问题了,接下来会等待锁,然后再次判断时不为null,最后返回单例。
如果读到的不是null,那么坏了,按逻辑它就直接return instance了,这个instance还没执行构造参数,去做事情的话,很可能就崩溃了。
加volatile
public volatile static Singleton instance;
public static Singleton getInstance()
{
if (instance == null) //1
{ //2
synchronized(Singleton.class) { //3
if (instance == null) //4
instance = new Singleton(); //5
}
}
return instance;
}
唯一的区别是加了volatile关键字,那么会有什么现象?
这时要区分jdk版本了,在jdk1.4及之前,volatile并不能保证new操作的有序性,但是它能保证可见性,因此标记1处,读到的不是null,导致了问题。
从1.5开始,加了volatile关键字的引用,它的初始化就不能是:
- a. 先分配内存,让instance指向这块内存
- b. 在内存中创建对象
而应该是:
- a.在内存中创建对象
- b.让instance指向这个对象.
这种形式,也就避免了无序性问题。
public class LazyInitDemo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) { synchronized(this) {
if (helper == null) { helper = new Helper();
} }
}
return helper; }
// other methods and fields...
}
Java中双重检查锁(double checked locking)的正确实现
前言
在实现单例模式时,如果未考虑多线程的情况,就容易写出下面的错误代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class Singleton { private static Singleton uniqueSingleton; private Singleton() { } public Singleton getInstance() { if ( null == uniqueSingleton) { uniqueSingleton = new Singleton(); } return uniqueSingleton; } } |
在多线程的情况下,这样写可能会导致uniqueSingleton有多个实例。比如下面这种情况,考虑有两个线程同时调用getInstance():
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到uniqueSingleton为空 | |
T2 | 检查到uniqueSingleton为空 | |
T3 | 初始化对象A | |
T4 | 返回对象A | |
T5 | 初始化对象B | |
T6 | 返回对象B |
可以看到,uniqueSingleton被实例化了两次并且被不同对象持有。完全违背了单例的初衷。
加锁
出现这种情况,第一反应就是加锁,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class Singleton { private static Singleton uniqueSingleton; private Singleton() { } public synchronized Singleton getInstance() { if ( null == uniqueSingleton) { uniqueSingleton = new Singleton(); } return uniqueSingleton; } } |
这样虽然解决了问题,但是因为用到了synchronized,会导致很大的性能开销,并且加锁其实只需要在第一次初始化的时候用到,之后的调用都没必要再进行加锁。
双重检查锁
双重检查锁(double checked locking)是对上述问题的一种优化。先判断对象是否已经被初始化,再决定要不要加锁。
错误的双重检查锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class Singleton { private static Singleton uniqueSingleton; private Singleton() { } public Singleton getInstance() { if ( null == uniqueSingleton) { synchronized (Singleton. class ) { if ( null == uniqueSingleton) { uniqueSingleton = new Singleton(); // error } } } return uniqueSingleton; } } |
如果这样写,运行顺序就成了:
- 检查变量是否被初始化(不去获得锁),如果已被初始化则立即返回。
- 获取锁。
- 再次检查变量是否已经被初始化,如果还没被初始化就初始化一个对象。
执行双重检查是因为,如果多个线程同时了通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象。
这样,除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问题。
隐患
上述写法看似解决了问题,但是有个很大的隐患。实例化对象的那行代码(标记为error的那行),实际上可以分解成以下三个步骤:
- 分配内存空间
- 初始化对象
- 将对象指向刚分配的内存空间
但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
- 分配内存空间
- 将对象指向刚分配的内存空间
- 初始化对象
现在考虑重排序后,两个线程发生了以下调用:
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到uniqueSingleton为空 | |
T2 | 获取锁 | |
T3 | 再次检查到uniqueSingleton为空 | |
T4 | 为uniqueSingleton分配内存空间 | |
T5 | 将uniqueSingleton指向内存空间 | |
T6 | 检查到uniqueSingleton不为空 | |
T7 | 访问uniqueSingleton(此时对象还未完成初始化) | |
T8 | 初始化uniqueSingleton |
在这种情况下,T7时刻线程B对uniqueSingleton的访问,访问的是一个初始化未完成的对象。
正确的双重检查锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class Singleton { private volatile static Singleton uniqueSingleton; private Singleton() { } public Singleton getInstance() { if ( null == uniqueSingleton) { synchronized (Singleton. class ) { if ( null == uniqueSingleton) { uniqueSingleton = new Singleton(); } } } return uniqueSingleton; } } |
为了解决上述问题,需要在uniqueSingleton前加入关键字volatile。使用了volatile关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。
至此,双重检查锁就可以完美工作了。
总结
到此这篇关于Java中双重检查锁(double checked locking)的文章就介绍到这了,更多相关Java双重检查锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
参考资料:
- 双重检查锁定模式
- 如何在Java中使用双重检查锁实现单例:http://www.importnew.com/12196.html
- 双重检查锁定与延迟初始化
南来地,北往的,上班的,下岗的,走过路过不要错过!
======================个性签名=====================
之前认为Apple 的iOS 设计的要比 Android 稳定,我错了吗?
下载的许多客户端程序/游戏程序,经常会Crash,是程序写的不好(内存泄漏?刚启动也会吗?)还是iOS本身的不稳定!!!
如果在Android手机中可以简单联接到ddms,就可以查看系统log,很容易看到程序为什么出错,在iPhone中如何得知呢?试试Organizer吧,分析一下Device logs,也许有用.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
2016-04-07 Impossible to load an image in xcassets on bundle