单例模式的初始化
面向对象设计模式(OO Design)有很多, 其中很重要的模式是单例模式。 和单例模式相关的另一个问题是这个单例在多线程环境下的实例化。 如何实例化的本身,也是另一个OO设计模式,网上相关的讨论很多,我做如下总结,供大家分享。
单例化的类有2个重要特征:
1)构造是私有的,这样可以保证外部无法实例化这个类
2)提供一个静态的方法,供外部调用以获得这个实例。
单例子模式的设计,单例类的代码基本如下:
public class SingletonClass {
public static SingletonClass getInstance() {
//如何返回这个实例,下面具体讨论。
}
private SingletonClass() {
//私有构造防止外部实例化这个类。
}
}
getInstance()的实现,有以下几个方案:
方案1:定义变量为final
最简单的方法是在这个类中定义一个静态的final的变量,final变量保证是线程安全的。
private static final SingletonClass instance = new SingletonClass();
在getInstance()中直接返回这个变量。
public static SingletonClass getInstance() {
return instance;
}
但这种做法的缺点是, 当JVM在加载这个类时, 会去构造instance。换句话说, 还不知道后面程序用得到用不到这个实例, 先构造了这个实例, 造成资源浪费。
我们程序中,早些时间写的程序,有这样的写法,但后面就逐渐没有了。
方案2:懒惰构造(lazy instantiation)
lazy instantiation 又称为Initialization-on-demand holder idiom,顾名思义就是在用到时在构造这个实例。 这在维基百科(http://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom)有介绍。懒惰构造的简单形式是
public synchronized static SingletonClass getInstance() {
if ( instance == null) {
instance = new SingletonClass();
}
return instance;
}
这个方案可以在用到时才实例化。但最大的问题是: 为了保证线程安全,需要加synchronized, 但synchronized有代价, 对单实例的实例化操作做synchronized,很不合算。去年,我们曾对这个lazy instantiation做了改进,采用了后来在网上才看到的所谓double checked locking 的设计模式。现在我们的程序都基本是用double checked locking了。
方案3 double-check locking
double checked locking的形式如下:
public static SingletonClass getInstance() {
if ( instance == null) {
synchronized ( lock ) {
if ( instance == null) {
instance = new SingletonClass();
}
}
}
return instance;
}
对instance做了两次判空,所以叫double-checked。 在instance不为空后,就不用锁了,所以效率比直接在方法前加synchronized要高得多。但这个方法有漏洞:Java Memory Model,将memory分成main memory(或称heap)和线程缓存(thread local memory)。执行一个线程时,从main memory读取值保存到thread memory,线程执行结束后,再将thread local memory的值返回给main memory。这样做的原因是,线程缓存通常是CPU的缓存,速度要比main memory快得多。但这样做的后果是,同一个变量在两个不同的线程,看到的值是不一样的(尤其是一个线程修改了变量的值,另一个线程有可能看不到这个值得变化)。这个问题在多线程程序中叫做visibility. 上述double check locking的问题就是,当一个线程分配空间给instance后,另一个线程看到这个空间,所以立即返回。但instance中的值的变化,第二个线程并没有看到。所以第二个线程返回的值是不完整的。在java 1.5后,volatile可以解决visibility的问题。
方案4: double-checked locking + volatile
java 1.5后对volatile的定义有了很大的变化: 当一个变量声明成volatile时,它能保证:instance只有在构造全部结束后(也即分配空间+给变量赋值全部结束), 才会将变量复制回mainmemory, 并且是立即送回(不需要等到线程结束才送回), 而另一个线程用instance时, 一定会去main memory取值, 不会用线程缓存的值。 有了这两个保证后, 第2个线程要不就看不到instance已经生成, 要不就看到一个完全生成的instance, 不会出现看到一个部分赋值的instance。 所以,在java 5后, 如果对instance增加volatile的定义, double check locking是安全有效的。 但java 4以前, 因为volatile没有这个功能, 所以double check locking是有漏洞的。
这个方案的缺点是: 1)volatile变量比正常变量效率低(线程不能用高速的线程内存), 所以整体对效率是有影响, 但影响没有synchronized大。2)这个方法读起来费劲。
方案5:lazy holder模式。
lazy holder模式就是对方案1的改进,它不用synchronized,也不用volatile,所以没有上述的开销。lazy holder模式的代码如下:
public class SingletonClass {
public static SingletonClass getInstance() {
return SingletonClassHolder.instance;
}
private SingletonClass() {
//私有构造防止外部实例化这个类。
}
private interface SingletonClassHolder {
public static final SingletonClass instance = new SingletonClass();
}
}
这个模式的重点就是将final的实例藏在一个内部的interface中。引进一个interface做holder的作用就是:当JVM载入SingletonClass这个类时,不会立即构造instance,
而方案1会。这个方法很简单,也没有开销。
//////////////////////////////总结///////////////////////////////////////
单例模式的实例化,按性能高低排,依次是:
lazy holder > double checked locking > synchronized
现在我们系统中已经使用了double checked locking,对这些地方,需要对变量加volatile
如果是新的代码,要用lazy holder模式实例化。