设计模式之单例模式

单例模式的定义:Ensure a class has only one instance, and provide a global point of access to it.确保一个类只有一个实例,并提供一个全局的访问点。

单例是比较简单的一种设计模式,简单到只有一个类图:

根据定义,我们设计一个类:

public class Singleton {

//仅此一个对象
	private static final Singleton instance = new Singleton();
	
	//构造方法私有化,确保只有这个类自身能实例化对象
	private Singleton(){
		
	}
	
	//通过此public方法获取对象
	public static Singleton getInstance() {
		return instance;
	}
	
}

它完全符合定义:私有化构造方法可以确保只有类自己实例化对象,从而保证了只有一个实例;public static的getInstance方法提供了一个全局的访问点。

对于单例的写法有两种,有的人称之为饿汉式和懒汉式,其实就是正常实例化和延迟实例化。懒汉和饿汉太难听了,我们不这么叫了。上面的例子就是正常实例化,即在JVM加载类的时候创建一个唯一的实例。但是想想这样做是有一些缺点的,如果这个实例在我们的应用中可能用不到岂不是白白浪费了?如果实例化过程中比较耗费资源, 负担过重,就不应该发生在JVM加载类的时候。解决方案就是延迟实例化,下面的程序是经典的延迟实例化单例模式实现:

 

public class SingletonLazyInitialization {

	//唯一实例
	private static SingletonLazyInitialization instance = null;
	
	//构造方法私有化
	private SingletonLazyInitialization(){}
	
	//通过此public方法获取对象
	public static SingletonLazyInitialization getInstance() {
		if (null == instance) {
			//初次调用此方法时,产生一个实例
			instance = new SingletonLazyInitialization();
		}
		return instance;
	}
	
}
 这个写法在并发量较小时候,问题不是很大。但是并发量大时可能会出现bug:如果线程A执行到了instance = new SingletonLazyInstance();线程B执行到了if (null == instance),由于线程A还没有完成实例化,那么线程B也会再次执行instance = new SingletonLazyInstance();此时出现了两个实例,因此单例模式失败了...

 

处理多线程的方案

方案一,在getInstance方法同步

 

public static synchronized SingletonLazyInitialization getInstance()
 这样解决多线程导致多实例是没问题的,方法上加synchronized会迫使每个线程进入方法之前等待前面的线程从方法中出来。但是性能问题随之而来,同步一个方法可能导致此方法的性能下降100倍!除非你的程序不考虑这些问题,否则没理由使用此解决方案。

 

方案二,并使用双重检查加锁

public static SingletonLazyInitialization getInstance() {
		if (null == instance) {
			//初次调用此方法时,产生一个实例
			synchronized (SingletonLazyInitialization.class) {
				if (null == instance) {
					instance = new SingletonLazyInitialization();
				}
			}
		}
		return instance;
	}
只有instance为空时候才进行加锁,很好的解决了性能问题。而且在synchronized块儿里面又进行了一次判断,貌似不会出现多实例的情况。但是...JVM在实例化一个对象时,大概有分配内存、初始化对象、将引用指向这个对象三个步骤要走,整个过程不是原子操作的,而且JVM不保证这三个步骤的顺序,因此有种情况就可能发生:线程A进入synchronized块儿执行instance = new SingletonLazyInstance(),JVM分配了内存,将引用指向了这个对象,但是对象并没有初始化完成;而此时线程B执行getInstance方法,发现instance已经不为null了,也就return一个为完成的对象,使用这样的对象会有问题啊。。。
对于这个问题,从JDK5开始我们有了解决方案,用volatile修饰instance对象:
private static volatile SingletonLazyInitialization instance = null;
 volatile能保证happens-before关系,对instance对象的写操作都发生在读操作之前,保证多线程能正确的处理instance。 volatile水比较深,深入介绍请参考http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

 双重检查加锁单例最好的写法是

public class SingletonLazyInitialization {

	//用volatile修饰保证不会出现半初始化对象
	private static volatile SingletonLazyInitialization instance = null;
	
	private SingletonLazyInitialization(){}
	
	//通过此public方法获取对象
	public static SingletonLazyInitialization getInstance() {
		if (null == instance) {
			//初次调用此方法时进入synchronized块
			synchronized (SingletonLazyInitialization.class) {
				if (null == instance) {
					instance = new SingletonLazyInitialization();
				}
			}
		}
		return instance;
	}
	
}

 volatile也有缺点:有一定的性能损耗,虽然相对于synchronized相比小得多。volatile不适用与jdk1.5之前的版本,导致双重检查加锁失败。

双重检查模式写起来比较复杂,有另外一种写法也开始实现延迟实例化,却不用double check麻烦:

 

/**
 * 延迟初始化holder class模式
 */
public class SingletonLazyInitializationInner {

	private SingletonLazyInitializationInner(){}
	
	//定义一个静态内部类作为holder class
	private static class SingletonLazyInitializationHolder {
		private static final SingletonLazyInitializationInner instance = new SingletonLazyInitializationInner();
	}
	
	//全局访问点
	public static SingletonLazyInitializationInner getInstance() {
		return SingletonLazyInitializationHolder.instance;
	}
	
}

 

这种方法叫lazy initialization holder class模式,根据JVM的机制规定,只有getInstance方法第一次被调用时内部类才被加载,从而实现了延迟实例化。加载过程是线程安全的:JVM保证类在初始化时,同步对变量的访问,一旦类初始化完成,JVM对后续的访问不会设置同步。

这种模式最大的优点是getInstance方法没有没有被同步,instance也没有被volatile修饰,所以没有增加任何成本。

单例模式的优点

  • 内存中只有一个实例,减少了内存开支,当可能存在很多实例时,使用单例节约内存。
  • 只生成一个对象,减少了实例化过程的开支,尤其是当一个对象实例化过程消耗很多的资源,例如Hibernate的SessionFactory对象,要读取配置文件、设置数据库连接池等等

单例模式的缺点:

  • 很明显一的,单例违反了单一职责原则。一个类做了自己本不应该做的事情,实例化自己。
  • 单例模式无法被继承,因为构造方法是私有的

单例模式的应用场景

  • 对于不存在线程安全的类,尽量用单例模式,例如我们用spring管理bean,很多都是单例的。
  • 创建对象消耗资源过多:用hibernate时,如果每次都创建一个SessionFactory对象,要疯了,必须是单例。
  • 需要一个全局共享的数据,比如一个访问计数器

当然更多的还要结合项目的实际情况。

 

总结:

  1. 正常实例化和延迟实例化的抉择:正常初始化优先于延迟实例化,除非很有必要(实例化开销大而且不能接受类初始化性能低、或者此实例可能根本用不到),否则不要选择延迟实例化。延迟实例化虽然提高类初始化性能,但是降低了访问被延迟实例化变量的性能。
  2. 如何选择延迟实例化的写法:对于单例模式,延迟实例化选择lazy initialization holder class模式,它最优秀。但它不适用于实例变量延迟初始化,只能选择双重检查加锁+volatile修饰模式。

一个谣言:在instance没有被引用时候会被垃圾收集器清理掉

其实这只是jdk1.2版本之前的一个bug:即在只有单例类引用单例对象时,单例对象会被垃圾收集器回收掉。

你可能感兴趣的:(设计模式)