摘自《深入浅出设计模式》(郭峰 著)
找到一篇更详细的文章http://crud0906.iteye.com/blog/576321
单例模式应该是所有设计模式中最简单的一种了,它的实现方法很简单,就是定义一个该类的静态变量,然后再定义一个获取该静态变量的静态方法。示意代码如下:
public class Singleton {
/**
* 饿汉式
*/
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
需要注意的是,在单例模式中,需要将构造函数定义为private,以防止可以通过构造函数获取该类的实例。
上述单例模式被称为饿汉式单例模式。对于单例模式,还有另外一种展示方式,示意代码如下:
public class Singleton {
/**
* 懒汉式
*/
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
上述示意代码被称为懒汉式单例模式,和饿汉式单例模式的区别就在于创建实例的时机,饿汉式单例模式是在该类加载时即被创建,而懒汉式单例模式则是在需要获取时才进行创建。
前面介绍的懒汉式单例模式的使用方法,在单线程的程序应用中是没有任何问题的,但是在多线程的程序中就会出现问题,当多个线程都进行if(instance == null)判断时,就会产生多个该类的实例,这就违背了单例模式的原则,也不符合程序的需要,怎么办呢?对于多线程下的应用,可以增加synchronized(同步)机制,示意代码如下:
public class Singleton {
/**
* 同步锁
*/
private static Singleton instance = null;
private Singleton(){
}
//增加同步机制
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
此时,将synchronized(同步)机制放在了获取实例的方法上,如果该程序在多个线程下执行,将会只产生一个该类的实例,达到了单例模式的要求,但是因为这种写法是将synchronized(同步)机制放在了获取实例的方法上,导致程序每获取一次实例,都将进入synchronized(同步)机制,如果在程序运行时,需要大量的获取该类的实例,这种方法将是非常低效的。还有另外一种写法,将synchronized(同步)机制放在产生实例的代码前,示意代码如下:
public class Singleton {
/**
* 同步锁
*/
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class) { //增加同步机制
instance = new Singleton();
}
}
return instance;
}
}
这种写法避免了每次获取实例时,都进入synchronized(同步)机制,但是和最初的写法一样,采用这种写法则避免不了在多线程时返回多个实例的问题,为此产生了新的写法Double-checked locking(双检测锁)机制,示意代码如下:
public class Singleton {
/**
* 双检测锁
*/
private static volatile Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class) { //增加同步机制
if(instance == null){ //对是否为null再次进行判断
instance = new Singleton();
}
}
}
return instance;
}
}
这种方式下,只有第一次创建实例时才进入synchronized(同步)机制,以后因为实例已经创建,将不会再进入synchronized(同步)机制,因此这种做法满足了在多线程条件下单例模式的应用。看起来这样已经达到了我们的要求,除了第一次创建对象之外,其他的访问在第一个if中就返回了,因此不会走到同步块中。已经完美了吗?
我们来看看这个场景:假设线程1执行到instance = new Singleton()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:
1.给Singleton的实例分配内存。
2.初始化Singleton的构造器
3.将instance对象指向分配的内存空间(注意到这步instance就非null了)。
但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程2上,这时候instance因为已经在线程1内执行过了第三点,instance已经是非空了,所以线程2直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来,真是一茶几的杯具啊。
DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonKerriganD instance = null;”就可以保证每次读取instance都从主内存读取,就可以使用DCL的写法来完成单例模式。由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字,最重要的是我们还要考虑JDK1.42以及之前的版本,所以本文中单例模式写法的改进还在继续。
再来看看下面这种能应对较多场景的单例写法:
/**
* 能应对较多场景的单例写法
*/
public class Singleton {
/** private的构造函数用于避免外界直接使用new来实例化对象 */
private Singleton() {
}
private static class SingletonHolder {
/** 单例对象实例 */
static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
这种写法仍然使用JVM本身机制保证了线程安全问题;由于SingletonHolder是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK版本。