单例模式
1 什么是单例模式
单例模式(Singleton Pattern),顾名思义,就是被单例的对象只能有一个实例存在 。单例模式的实现方式是,一个类能返回对象的一个引用(永远是同一个)和一个获得该唯一实例的方法(必须是静态方法)。通过单例模式,我们可以保证系统中只有一个实例,从而在某些特定的场合下达到 节约或者控制系统资源的目的 。
2 单例模式示例代码
1.饿汉模式
最常见、最简单的单例模式写法之一。顾名思义,“饿汉模式” 就是很 “饥渴”,所以一上来就需要给它新建一个实例。缺点很明显:线程不安全,不能用于多线程环境,不管有没有调用过获得实例的方法(本例中为 getWife() ),每次都会新建一个实例。
// 饿汉模式
public class Wife {
// 一开始就新建一个实例
private static final Wife wife = new Wife();
// 默认构造方法
private Wife() {}
// 获得实例的方法
public static Wife getWife() {
return wife;
}
}
2.懒汉模式
最常见、最简单的单例模式之二,跟 “饿汉模式” 是 “好基友”。再次顾名思义,“懒汉模式” 就是它很懒,一开始不新建实例,只有当它需要使用的时候,会先判断实例是否为空,如果为空才会新建一个实例来使用。
// 懒汉模式
public class Wife {
//一开始没有新建实例
private static Wife wife;
private Wife() { }
// 需要时再新建
public static Wife getWife() {
if (wife == null) {
wife = new Wife();
}
return wife;
}
}
3.线程安全的懒汉模式
但是懒汉模式却存在一个严重的问题。那就是如果有多个线程并行调用 getWife() 方法的时候,还是会创建多个实例,单例模式就失效了。
我们在基本的懒汉模式上,把它设为线程同步(synchronized)就好了。synchronized 的作用就是保证在同一时刻最多只有一个线程运行,这样就避免了多线程带来的问题。不过代价是性能会受到影响,因为大部分时间的操作其实不需要同步。
// 懒汉模式(线程安全)
public class Wife {
private static Wife wife;
private Wife() { }
// 添加了 synchronized 关键字
public static synchronized Wife getWife() {
if (wife == null) {
wife = new Wife();
}
return wife;
}
}
4.双重检验锁(double check)
线程安全的懒汉模式解决了多线程的问题,看起来完美了。但是它的效率不高,每次调用获得实例的方法 getWife() 时都要进行同步,但是多数情况下并不需要同步操作(例如我的 wife 实例并不为空可以直接使用的时候,就不需要给 getWife() 加同步方法,直接返回 wife 实例就可以了)。所以只需要在第一次新建实例对象的时候,使用同步方法。
于是,在前面的基础上,又有了 “双重检验锁” 的方法。
// 双重锁的 getWife() 方法
public static Wife getWife() {
// 第一个检验锁,如果不为空直接返回实例对象,为空才进入下一步
if (wife == null) {
synchronized (Wife.class) {
//第二个检验锁,因为可能有多个线程进入到 if 语句内
if (wife == null) {
wife = new Wife();
}
}
}
return wife ;
}
直接同步整个getWife()方法产生性能低下的原因是,在判断(wife == null)时,所有线程都必须等待。而(wife == null)并非是常有情况,每次判断都必须等待,会造成阻塞。因此,有了这种双重检测的实现方法,待检查到实例没创建后(wife == null),再进行同步,然后再检查一次确保实例没创建。
在同步块里,再判定一次,是为了避免线程A准备拿到锁,而线程B创建完wife后准备释放锁的情况。如果在同步块里没有再次判定,那么线程A很可能会又创建一个实例。
另外,再引用IcyFenix文章里面的一段话,会解释清楚双锁检测的局限性:
我们来看看这个场景:假设线程一执行到wife = new Wife();这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:
1.给new Wife() 的 Wife的实例分配内存。
2.初始化new Wife() 的 Wife的构造器
3.将wife对象指向分配的内存空间(注意到这步wife就非null了)。
但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候wife因为已经在线程一内执行过了第三点,wife已经是非空了,所以线程二直接拿走wife,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来,真是一茶几的杯具啊。
这个时候,我们需要给实例加一个 volatile 关键字,它的作用就是防止编译器自行优化代码。最后,我们的 “双重检验锁” 版本终于出炉了…
// 双重检验锁
public class Wife {
private volatile static Wife wife;
private Wife() { }
public static Wife getWife() {
if (wife == null) {
synchronized(Wife.class) {
if (wife == null) {
wife = new Wife();
}
}
}
return wife;
}
}
5.静态内部类
上面的方法,修修补补,实在是太复杂了… 而且 volatile 关键字在某些老版本的 JDK 中无法正常工作。咱们得换一种方法,即 “静态内部类”。这种方式,利用了 JVM 自身的机制来保证线程安全,因为 WifeHolder 类是私有的,除了 getWife() 之外没有其它方式可以访问实例对象,而且只有在调用 getWife() 时才会去真正创建实例对象。(这里类似于 “懒汉模式”)
// 静态内部类
public class Wife {
private static class WifeHolder {
private static final Wife wife = new Wife();
}
private Wife() { }
public static Wife getWife() {
return WifeHolder.wife;
}
}
6.枚举
《Effective Java, 2nd》第三条:enum是实现Singleton的最佳方法
我们可以通过 Wife.INSTANCE 来访问实例对象,这比 getWife() 要简单得多,而且创建枚举默认就是线程安全的,还可以防止反序列化带来的问题。
// 枚举
public enum Wife {
INSTANCE;
// 自定义的其他任意方法
public void whateverMethod() { }
}
3 单例模式的应用
当你只需要一个实例对象的时候,就可以考虑使用单例模式。比如在资源共享的情况下,避免由于多个资源操作导致的性能或损耗等就可以使用单例模式。
4 总结
单例模式,并不是整个程序或者整个应用只有一个实例,而是整个classloader只有一个实例。如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例
单例模式,会使测试、找错变得困难(根据《Effective Java, 2nd,第三条》) ,尝试使用DI框架(Juice/Spring)来管理。
什么情况下单例模式会失效(JPMorgan)?
Serialization, Reflection, multiple ClassLoader, multiple JVM, broken doubled checked locking(JDK4 or below) etc