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

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#server The 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对象,是与最初的需求不相符的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值