在开发工程中,单例模式是最常用,也是最简单的一种设计模式。
那么,一个完美的单例模式的实现需要做哪些事呢?
以上是实现一个单例模式需要考虑到的一些基本因素。下面就这些因素来讲下几种单例模式的实现方法。
这是最常用,也是最直接了当的一种方法
public class SingletonFirst implements Serializable {
//类加载时就创建实例
private static SingletonFirst instance = new SingletonFirst();
//私有化构造方法
private SingletonFirst() {
// 防止反射获取多个对象的漏洞
if (null != instance) {
throw new RuntimeException();
}
}
//对外获取实例的方法
public static SingletonFirst getInstance() {
return instance;
}
/*
防止反序列化获取多个对象的漏洞。
无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。
*/
private Object readResolve() throws ObjectStreamException {
return instance;
}
}
在懒汉式中,当类加载时就创建了单例对象,这样不仅对性能有一定影响,更严重的是如果该单例对象的创建依赖于其他一些配置参数,而在该类加载时这些参数还未加载,那么就很可能造成单例对象创建失败,而且无法通过getInstance()
方法传入参数,几时传入了也无效。当然,如果使用这种方式的话,线程绝对是安全的。如果通过反射创建对象的话,就会调用构造函数,此时在构造函数中判断单例对象是否为空,不为空的话就直接抛出异常。此外,readResolve()
方法是所有序列化与反序列化都会调用的方法,在该方法中将单例对象直接返回,这样就能解决序列化问题了。
为了避免饿汉式的一系列缺点,于是选择使用懒汉式
public class SingletonSecond implements Serializable {
private static SingletonSecond instance;
private SingletonSecond() {
// 防止反射获取多个对象的漏洞
if (null != instance) {
throw new RuntimeException();
}
}
public static SingletonSecond getInstance() {
if (null == instance) {
instance = new SingletonSecond();
}
return instance;
}
private Object readResolve() throws ObjectStreamException {
return instance;
}
}
在懒汉式中,多线程安全就是一个不得不考虑的问题了。如果在多线程中使用懒汉式,以上的getInstance()
方法是一个线程不安全的方法。当然,可以直接加synchronized
关键字来解决问题,如下所示
public static synchronized SingletonSecond getInstance() {
if (null == instance) {
instance = new SingletonSecond();
}
return instance;
}
这样就可以安全地解决线程问题了,但是,这样又带来了新问题,就是性能问题。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。代码如下
public static SingletonSecond getInstance() {
if (null == instance) {
synchronized (SingletonSecond.class) {
if (null == instance) {
instance = new SingletonSecond();
}
}
}
return instance;
}
这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
那么,该怎么解决呢??其实,只需要将 instance 变量声明成 volatile 就可以了。
// 声明成 volatile
private volatile static SingletonSecond instance;
//...
有人认为使用volatile的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。
使用静态内部类,能更好地实现单例模式,能同时解决线程安全问题和性能问题
public class SingletonFifth implements Serializable {
private SingletonFifth() {
// 防止反射获取多个对象的漏洞
if (null != SingletonHolder.INSTANCE) {
throw new RuntimeException();
}
}
public static SingletonFifth getInstance() {
return SingletonHolder.INSTANCE;
}
// 防止反序列化获取多个对象的漏洞
private Object readResolve() throws ObjectStreamException {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final SingletonFifth INSTANCE = new SingletonFifth();
}
}
这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
这是最快捷的单例模式实现方式
public enum SingletonSixth {
INSTANCE;
public void doSomething() {
//...
}
}
可以通过SingletonSixth.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,而且还能防止反序列化导致重新创建新的对象。但是这种方法有一个比较致命的缺陷,就是在获取单例对象时无法传入参数,而之前的所有方法(饿汉式除外)都可以在getInstance(Object... args)
方法中传入一些初始化参数。
一般来说,单例模式有四种写法:懒汉(包含双重检验锁)、饿汉、静态内部类、枚举。个人见解而已,不喜勿喷!!