java中singleton模式与延迟初始化实现方式总结

来源:互联网 发布:c语言复合语句 编辑:程序博客网 时间:2024/06/05 19:40

singleton模式是常用的设计模式之一。最原始的实现方式如下:

public class singleton {private static singleton instance;private singleton(){}public static singleton getInstance(){if(instance == null)instance = new singleton();return instance;}}

这种方式在多线程的情况下会出现实例化多个,比如如下调度模式:Thread 1Thread 2测试singleton==null通过  测试singleton==null通过执行new singleton()构造函数  执行new singleton()构造函数如此会出现两个singleton对象。

一个惯用的修复方式为给getInstance方法加上synchronized同步锁:

public class singleton {private static singleton instance;private singleton(){}public static synchronized singleton getInstance(){if(instance == null)instance = new singleton();return instance;}}
这个方式能够修复上面的问题,但是会带来较大的性能损失。因为每一次调用getInstance时都必须要进出synchronized块,而事实上除了第一次调用需要同步外,其他时候都不需要同步。为了解决这个问题,有了著名的“Double-Checked Locking”。

public class singleton {private static singleton instance;private singleton(){}public static singleton getInstance(){if(instance == null)synchronized(singleton.class){if(instance == null)instance = new singleton();}return instance;}}
但这种方式实际上是有问题的,问题出在instance = new singleton();这一句上,这一句实际上有若干语句构成。

使用javap -c singleton.class命令可以得到singleton类的字节码:

Compiled from "singleton.java"public class singleton {  public static singleton getInstance();    Code:       0: getstatic     #2                  // Field instance:Lsingleton;       3: ifnonnull     38       6: ldc_w         #3                  // class singleton       9: dup                 10: astore_0            11: monitorenter        12: getstatic     #2                  // Field instance:Lsingleton;      15: ifnonnull     28      18: new           #3                  // class singleton      21: dup                 22: invokespecial #4                  // Method "<init>":()V      25: putstatic     #2                  // Field instance:Lsingleton;      28: aload_0             29: monitorexit         30: goto          38      33: astore_1            34: aload_0             35: monitorexit         36: aload_1             37: athrow              38: getstatic     #2                  // Field instance:Lsingleton;      41: areturn           Exception table:       from    to  target type          12    30    33   any          33    36    33   any  static {};    Code:       0: aconst_null          1: putstatic     #2                  // Field instance:Lsingleton;       4: return        }
其中
 12: getstatic     #2                  // Field instance:Lsingleton;      15: ifnonnull     28      18: new           #3                  // class singleton      21: dup                 22: invokespecial #4                  // Method "<init>":()V      25: putstatic     #2                  // Field instance:Lsingleton;
为synchronized同步块中的操作。第18行为即将创建的singleton对象,但此时并没有调用构造函数。第22行invokespecial调用构造函数。第25行把该对象的引用赋值给instance。

如果代码严格按照上述顺序运行,则这个方法就没有问题。但是jvm在程序运行时会进行优化,一个典型的优化就是指令重排。指令重排能够使JVM根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。指令重排序的标准是在单线程下,保证程序的效果不变。


对synchronized同步块中的代码指令重排:

 12: getstatic     #2                  // Field instance:Lsingleton;      15: ifnonnull     28      18: new           #3                  // class singleton      21: dup       25: putstatic     #2                  // Field instance:Lsingleton;                22: invokespecial #4                  // Method "<init>":()V    

由于指令的重排序,第25行的给instance赋值可能先于22行的invokespecial的对构造函数的调用,导致instance虽然已被赋值而不为null,但其引用指向的对象并没有被正确初始化。有可能导致另一个线程调用getInstance时得到的是未被正确初始化的对象。

Thread 1Thread 2得到singleton.class的锁,进入同步块。 根据指令重排后的代码执行到putstatic,将new创建的singleton对象赋值给instance,但此时invokestatic语句并没有被执行,singleton的构造方法并没有被调用,该singleton对象并没有被正确初始化。  调用getInstance方法,测试instance不为null,因此返回instance,而此时instance指向的singleton对象的初始化并没有完成。 由于使用了未被正确初始化的singleton对象,程序可能出错。




针对这种问题,有一种改进的方法,试图绕开指令重排的干扰。但该方法是无效的。

public class singleton {private static singleton instance;private singleton(){}public static singleton getInstance(){if(instance == null)synchronized(singleton.class){singleton temp = null;if(instance == null)temp = new singleton();instance = temp;}return instance;}}

该方法使用一个临时变量来创建singleton对象,然后再赋值给instance。初看之下似乎可行,但是jvm在运行时可能把

temp = new singleton();instance = temp;
这两行代码优化成instance = new singleton();来执行,实际上与刚才没有差别。(来自http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization中的评论,指出JIT在运行时会进行所有经典的优化:http://www.oracle.com/technetwork/java/whitepaper-135217.html#serverThe optimizer performs all the classic optimizations, including dead code elimination, loop invariant hoisting, common subexpression elimination, constant propagation, global value numbering, and global code motion 上面这种方式应该属于common subexpression elimination

针对这种优化,又有一种改进:

public class singleton {private static singleton instance;private singleton() {}public static singleton getInstance() {if (instance == null)synchronized (singleton.class) {singleton temp = null;if (instance == null)synchronized (singleton.class) {temp = new singleton();}instance = temp;}return instance;}}
这个方法使用内嵌的synchronized同步块来隔开可能被优化的两行语句。但是这种方法仍然不可行。这是因为synchronized同步块的语法为同步块中的语句执行完前不会释放锁,并没有限定synchronized同步块后的语句必须在锁释放后执行。所以JVM可能把instance=temp放到同步块中,这样就回到了上一种情况。

针对上一种情况的又一种改进:

public class singleton {private static singleton instance;private singleton() {}public static singleton getInstance() {if (instance == null)synchronized (singleton.class) {singleton temp = null;if (instance == null)temp = new singleton();synchronized (singleton.class) {instance = temp;}}return instance;}}
这种方法几乎已经可行,但仍然有一些微妙的可能导致多个singleton对象被创建。因为对于多核处理器,每个线程都有自己的cache,可能一个线程已经创建了instance对象,而另一个线程的cache中保存的instance副本仍然是null,于是这个线程仍然可能继续创建一个新的singleton。不过这种行为在理论上存在,在实践中极难观察到。事实上这一种解释就能够推翻以上所有的Double-Checked Locking尝试。

以上两个方法及其无效的解释来自http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html




对于“Double-Checked Locking”,自java5收紧volatile的语意后,使用volatile关键字修饰instance能够修复以上的问题。

public class singleton {private static volatile singleton instance;private singleton(){}public static singleton getInstance(){if(instance == null)synchronized(singleton.class){if(instance == null)instance = new singleton();}return instance;}}
java5以后,对于以volatile修饰的变量,对其的读写都不可指令重排,而且volatile亦保证了线程不会因为cache而创建多个对象。

此外,这个方法可以优化性能:

public class singleton {private static volatile singleton instance;private singleton(){}public static singleton getInstance(){singleton result = instance;if(result == null)synchronized(singleton.class){result = instance;if(result == null)instance = result = new singleton();}return result;}}
将instance读取到result中,保证volatile修饰的变量只读取一次,可以提高性能。此方法来自Effective Java

以上方法亦可使用在延迟初始化实例的属性上,只要将属性和get方法上的static去掉即可。

对于java5之前的jvm,就必须在以下两个选项中权衡选择:

1.在get方法上使用synchronized

public class singleton {private attribute instance;public synchronized attribute getInstance(){if(instance == null)instance = new attribute();return instance;}}class attribute{}
接受其性能上的损失。

2.在对象初始化时初始化该属性

public class singleton {private attribute instance = new attribute();public attribute getInstance() {return instance;}}class attribute {}
放弃延迟初始化。


对于延迟初始化静态变量和单例模式,可以使用lazy initialization holder class模式(也称作initialize-demand holder class idiom模式),出自Effective Java

单例模式:

public class singleton {private singleton(){}private static class instanceHolder{static singleton instance = new singleton();}public static singleton getInstance(){return instanceHolder.instance;}}
延迟初始化静态变量:

public class singleton {private static attribute instance;private static class instanceHolder{static attribute instance = new attribute();}public static attribute getInstance(){return instanceHolder.instance;}}class attribute{}
注意这个方法,去掉属性和get方法上的static修饰符,通过对象调用get方法似乎也能运行。但这样的话,对于不同的对象,属性instance都指向了同一个attribute对象,是与最初的需求不相符的。

原创粉丝点击