单例模式是应用最广的设计模式之一,也是写法和变种最多的一种设计模式。单例对象要确保在全局中只存在一个。这种模式适合消耗资源严重的使用场景。
实现单例模式主要注意以下几个关键点:
1)构造函数不对外开放,一般为private;
2)通过一个静态方法或枚举返回单例类对象;
3)确保单例类的对象有且只有一个,尤其在多线程的环境下;
4)确保单例类对象在反序列化时不会重新构建对象。
下面通过代码演示单例模式的几种写法:
饿汉式
废话不说,直接上代码
//饿汉式单例
public class SingleTon {
private static SingleTon singleTon=new SingleTon();
private SingleTon(){
}
public static SingleTon getInstance(){
return singleTon;
}
}
这是最简单的单例模式的实现。这种方式创建的对象,在声明对象的时候就已经初始化,但是如果程序运行过程中没有使用到,就白白浪费了内存。为了弥补这样的缺陷,就产生了懒汉式单例模式。
懒汉式
//懒汉式单例
public class SingleTon {
private static SingleTon singleTon;
private SingleTon(){
}
public synchronized static SingleTon getInstance(){
if (singleTon==null){
singleTon=new SingleTon();
}
return singleTon;
}
}
这种写法保证了在第一次调用getInstance()才对singleTon实例进行初始化。在一定程度上节约了资源;但是在第一次加载的时候需要进行实例化,反应会变慢,还有就是每次调用getInstance都会进行同步,造成了不必要的锁开销。
双重检查锁(DCL)
如果既要保证在需要时才初始化单例,又要保证线程安全,同时单例对象初始化后调用getInstance()不进行同步锁,那么就产生了DCL单例
//DCL单例
public class SingleTon {
private static volatile SingleTon singleTon;
private SingleTon() {
}
public static SingleTon getInstance() {
if (singleTon == null) {
synchronized (SingleTon.class) {
if (singleTon == null) {
singleTon = new SingleTon();
}
}
}
return singleTon;
}
}
上面这段代码中,getInstance()方法做了2次判空,第一次判空主要是考虑到singleTon在已经加载过的情况下避免不必要的同步,第二层判断是考虑到singleTon在null的情况下创建实例。这里为了保证singleTon = new SingleTon()能够被同步执行,在外层添加了synchronized锁。但是Java编译器允许处理器可以乱序执行,会导致getInstance()没有在线程A上执行完,而被切换到线程B上,线程B直接取走singleTon造成使用出错,这就是DCL失效问题,为了解决这个问题,在JDK1.5以后,Java官方调整了JMM,修正了volatile关键字的语义,只要将singleTon设置成volatile属性,就可以保证singleTon对象每次都是从主内存中读取,保证了DCL的正确性。
DCL模式能够在绝大多数场景下保证单例对象的唯一性,也弥补了以上2种单例模式的缺陷。是使用最多的单例模式。
静态内部类
DCL在一定程度上解决了资源消耗,多余的同步,线程安全等问题,但是在JDK1.5以下还会出现DCL失效的问题。而且代码对于初级工程师来说比较难懂,所以出现了静态内部类的单例模式
//静态内部类
public class SingleTon {
private SingleTon() {
}
private static class SingletonHolder{
private static SingleTon singleTon=new SingleTon();
}
public static SingleTon getInstance(){
return SingletonHolder.singleTon;
}
}
当第一次加载SingleTon类的时候并不会初始化singleTon,只有在第一次调用getInstance()的时候才会初始化singleTon实例。第一次调用getInstance()方法会导致虚拟机加载SingletonHolder类,这样不仅保证了线程安全,也保证了单例的唯一性和延迟加载特性。
枚举
上述的几种单例实现都会在一个情况下重新创建对象实例,那就是反序列化。反序列化时通过特殊的途径去创建类的一个新的实例,相当于调用了该类的构造函数。反序列化操作提供了一个特别的回调函数,即一个私有、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。
private Object readResolve() throws ObjectStreamException{
// 此处返回单例模式中的实例对象
return singleTon;
}
这样在readResolve方法中返回当前实例,而不是重新生成对象,在反序列化时也能保证单例。
还有一种方式,也不存在反序列化的问题,那就是通过枚举。默认枚举创建的实例是线程安全的,并且在任何情况下都是单例。
//枚举
public enum SingletonEnum{
SINGLETON;
public void method(){
//do something...
}
}
除了上面的几种单例创建方式,还有其他的一些单例写法,比如静态代码块,但是原理都包含在上面的几种写法里。单例模式的核心原理都是将构造函数私有化,并通过静态方法获得唯一的实例。并且根据项目需要选择适合的单例方式。