双重检查锁定模式(也被称为"双重检查加锁优化","锁暗示") 是一种软件设计模式用来
减少并发系统中竞争和同步的开销
。双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)
。
该模式在某些语言在某些硬件平台的实现可能是不安全的。有的时候,这一模式被看做是反模式。
它通常用于减少加锁开销,尤其是为多线程环境中的单例模式
实现“惰性初始化
”。惰性初始化的意思是直到第一次访问时才初始化它的值。
---- from wikipedia
- 最开始的代码:
-
在多线程环境下运行这份代码将会造成很多错误,最明显的问题是(其他问题在后面):
假设有线程1,2,当线程1运行到A进入B区将要新建实例时,(此时helper还未赋值)线程2也通过A的判断语句,进入B区新建实例.这样就造成了两个或多个helper被实例化.
// Single threaded version class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) //A helper = new Helper(); //B return helper; //C } // other functions and members... }
- 加锁代码:
-
对
代码1
最简单的处理方式是对方法加锁
进行同步处理:// 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... }
- 双重检查锁定代码:
-
代码2
当中每次获取helper都要进行加解锁操作,开销是很大的.而除了第一次实例化对象,其他时候都只是单纯放回helper对象,不需要同步操作,于是有了代码3
:// 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... }
-
【我是重点】然而问题来了,这份代码在
编译器
和共享内存多处理器
的优化(即重排序)下是无效的. -
为什么无效?有两个原因,第一个原因(第二个原因看
代码4
):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.
大致意思是Helper对象的初始化
(这里特指<init>
方法,注意区分对象初始化
与类初始化
)和对象写入helper域
可以是无序的.因此,一个调用getHelper()的线程可以看到helper对象的非空引用,但是看到的helper的字段是默认值
(零值),而不是在构造函数中设置的值。(参考对象创建过程)singletons[i].reference = new Singleton();这段代码在
Symantec JIT编译
后如下:
显然,对象的分配
发生在对象初始化
之前,这在Java内存模型中是合法的.0206106A mov eax,0F97E78h 0206106F call 01F6B210 ; 为单例分配空间 ; 结果返回到eax 02061074 mov dword ptr [ebp],eax ; EBP 是 &singletons[i].reference ; 将未执行<init>操作的对象存放到此处 02061077 mov ecx,dword ptr [eax] ; 大致是获取原始指针的操作 02061079 mov dword ptr [ecx],100h ; 接下来四行是 0206107F mov dword ptr [ecx+4],200h ; 单例的<init>操作 02061086 mov dword ptr [ecx+8],400h 0206108D mov dword ptr [ecx+0Ch],0F84030h
- "改进"版代码:
-
基于上述问题,人们又给出了改进版的代码:
把对象实例化放入内部的同步代码块
中,通过一个内部锁,认为这样可以强制对象实例化
完成后才能将对象的引用存入helper域中.// (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... }
-
然而,这种想法也是错误的!!!
monitorexit
(弹出objectref,释放和objectref相关联的锁的操作码)的规则是:monitorexit
操作码之前的操作必须在锁释放之前执行.
但并没有规定说明monitorexit
之后的操作不能在锁释放之前执行.所以编译器移除helper = h
是完全合理的.
许多处理器提供了这种单向内存栅栏
的说明,强行改变它的语义为双向内存栅栏
将会得到错误的运行结果.
将单例定义为一个单独的类中的静态字段
.Java的语义保证字段不会被初始化,直到字段被引用,并且任何访问字段的线程都将看到所有的初始化该字段的值.
// 1
class HelperSingleton {
static Helper singleton = new Helper();
}
class Foo {
public Helper getHelper() {
return HelperSingleton.singleton;
}
}
//2
class Foo {
private static class HelperSingleton {
static Helper singleton = new Helper();
}
public static Helper getHelper() {
return HelperSingleton.singleton;
}
}
-
使用
线程本地存储
实现双重检查锁定
的巧妙方法.
每个线程保留一个线程本地标志
来确定该线程是否完成了所需的同步。但这取决于LocalThread
的存取速度.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); } }
-
更好的方法: 使用
volatile
:
系统不允许volatile写操作
与之前的任何读写
操作重排序,不允许volatile读操作
与之后的任何读写
操作重排序.
通过声明实例属性是volitile
的,保证(对象初始化
在实例分配
之前发生)双重检查锁定.
// 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类似。读取和写入对不可变对象的引用是原子操作。