单列模式是几乎成为了所有模式的入门课程。因为它又“简单”,又“复杂”。作为引入模式这个概念来说确实是不错的。不过经典 Singleton 在多线程时,并不是那么可靠,那么如何保证单例模式的线程安全呢?
Singleton,单例,顾名思义,是只存在一个的意思。程序里为了保证一致性,我们需要所有的操作都是一个唯一的对象来做。
1. 先来看看经典单列
它们的的特点是:
(1) static 的实例变量
(2) private 的构造函数
(3) 使用 getInstance() 返回对象
1.1 Eager initialization singleton:
class EagerSingleton { private static EagerSingleton instance = new EagerSingleton(); private EagerSingleton() { } public static EagerSingleton getInstance() { return instance; } }
Eager initialization singleton 的 instance 在类初始化时initialized,并且 static 保证了 instance 只被初始化一次。单例模式是线程安全的。它只有一个被诟病的,就是 instance 初始化太早了。因为直到调用 getInstance() 方法之前,都是不需要 instance 的。
1.2 lazy initialization singleton:
class LazySingleton { private static LazySingleton instance; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) instance = new LazySingleton(); return instance; } }
Lazy initialization singleton 唯一的不同是将初始化推迟到了 getInstance() 调用的时候。效率上来说,当然更好,不过有一个严重的问题,就是多线程时不再安全。如果两个线程同时进入 if(instance == null) 那么他们会生成两个对象。
2. Initialization on demand holder idiom
public class Singleton { private Singleton() { } private static class SingletonHolder { public static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
这是一个比较新的单例实现方式,属于 Eager initialization singleton 的变种。实例对象 INSTANCE 仅当 getInstance() 调用的时候才会被初始化。 而静态类 SingletonHolder 在实例化时,由JVM保证了仅被初始化一次。 没有使用 volatile 和 synchronized。是相当简洁又有效的最佳实践,推荐使用。
3. Double-checked locking
Double-Check 是 Lazy initialization singleton 的变种。 我们一步步来看看经典的 Double-Check 是如何实现的。
首先直接加上 synchronized 的关键字 :
public static synchronized Singleton getInstance(){ if (instance == null) instance = new Singleton(); return instance; }
这是正确的写法,它能够保证线程安全。但是效率不高。因为每一次调用 getInstance() 方法都用 synchronized 做了同步块操作。 而其实我们只想在 instance 第一次被初始化的时候做 lock 而已。所以需要改进。
现在我们仅当 instance == null 的时候初始化
public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { instance = new Singleton(); } } return instance; }
问题是如果 thread1和thread2同事进入 if (instance == null ) 那么还是会分别实现自己的实例。Ok,现在终于到了经典的 Double-Checked Locking了:
public static Singleton getInstance() { if (instance == null) { // 1 synchronized(Singleton.class) { if (instance == null) // 2 instance = new Singleton(); } } return instance; }
看起来一切都符合逻辑了!! 可惜 不是 .... 因为JVM的实现各有不同,有时会出现 “Out-of-order writes ”
Out-of-order writes Problem:
mem = allocate(); //Allocate memory for Singleton object. instance = mem; //Note that instance is now non-null, but has not been initialized. ctorSingleton(instance); //Invoke constructor for Singleton passing instance.
即是说,thread1 和 thread 2 同时进入 //1 . 然后thread1先进入 //2 的位置,然后开始初始化,但是初始化还没有完成。 这时 thread2 发现 instance 不为null (thread1还没有完成的初始化) 就直接返回了。
关于这种情况,有人建议是 使用 volatile (variables declared volatile are supposed to be sequentially consistent, and therefore, not reordered):
public static volatile instance = null;
但是 volatile 的问题是,许多 JVM 并没有按照 sequential consistency 正确的实现 volatile . 所以即便使用了 也可能失效
本文参考资料:
http://www.ibm.com/developerworks/java/library/j-thread.html
http://en.wikipedia.org/wiki/Singleton_pattern
http://www.ibm.com/developerworks/java/library/j-dcl.html