7. Double-Checked Locking
双重检查锁定机制,是一个老生常谈的问题了。双重检查锁定机制已经被广泛的引用,特别是在多线程环境下的懒加载实现上。但是,如果没有额外的同步,它不能独立可靠的运行在Java平台。看这段代码:
public class Singleton {
private Singleton instance = null;
public Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
// other function
}
如果这段代码在多线程的环境下运行,它会导致很多的错误。最明显的就是,Singleton对象会被两次或两次以上的实例化。为了解决这个错误,我们只需要为getInstance()方法加上synchronized关键字,改写后的类如下:
public class Singleton {
private Singleton instance = null;
public synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
// other function
}
现在,每次调用getInstance()方法的时候,都会进行同步。双重检查锁机制就是为了避免instance不为null时产生额外的同步开销,所以有了一下代码:
public class Singleton {
private Singleton instance = null;
public Singleton getInstance(){
if(instance == null){
synchronized(this){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
// other function
}
可是,如果这段代码在经过优化的编译器或共享内存多处理器的存在情况下运行的话,是行不通的。主要是因为旧的Java内存模型导致,调用getInstance()方法后,看到非空instance 引用,但是其Singleton所有属性均为默认值,而非在构造函数中指定的值。如果编译器能够保证其构造函数不抛出异常或执行同步,编译器内联的调用了构造函数,那么对于初始化instance和其所有属性的写操作将重序。即使编译器不会重序,在一个多核处理或内存系统中也会进行重序。所以就有了如下的改善:
public class Singleton {
private Singleton instance = null;
public Singleton getInstance(){
if(instance == null){
Singleton s ;
synchronized(this){
s = instance;
if(s == null){
synchronized(this){
s = new Singleton();
} // release inner synchronization lock
}
instance = s;
}
}
return instance;
}
}
引用
This code puts construction of the Singleton 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 instance = s; 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.
Alexander Terekhov想出了一个非常巧妙的问题解决办法,通过ThreadLocal变量来实现双重检查锁机制问题。
public class Singleton {
/** If perThreadInstance.get() returns a non-null value, this thread
has done synchronization needed to see initialization
of helper */
private final ThreadLocal<Singleton> local = new ThreadLocal<Singleton>();
private Singleton instance = null;
public Singleton getInstance(){
if(local.get() == null){
createInstance();
}
return instance;
}
private final void createInstance(){
synchronized(this) {
if (instance == null)
instance = new Singleton();
}
local.set(instance);
}
// other function
}
这段代码的性能取决于JDK的实现。在JDK1.2,ThreadLocal非常的慢,1.3就有了明显的提升。虽然这段代码,在现在看来是错误的(因为现在新的Java内存模型),但是在当时,确实解决了双重检查锁机制导致的问题。
自JDK1.5后,新的Java内存模型和线程规范后,可以通过volitale关键字,防止读写重序,解决双重检查锁机制导致的问题:
public class Singleton {
private volatile Singleton instance = null;
public Singleton getInstance(){
if(instance == null){
synchronized(this){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
// other function
}
如果Singleton是不可变的对象,且其所有属性都是不可变的,那么就不需要volatile关键字修饰了。Java语言规定,因为对于一个不可变对象的引用,其行为和int,float是一样的,对其的读和写操作都是原子性的。
然而最简单高雅的方式创建instance莫过于使用static:
class Singleton {
private static Singleton instance = new Singleton();
}
Java语言规定,字段的初始化是从被引用后,而且所有的线程将从字段初始化后进行相应的操作。
Reference :
http://www.cs.umd.edu/~pugh/java/memoryModel/
http://gee.cs.oswego.edu/dl/cpj/jmm.html