单例模式的5种写法及分析
一、饿汉模式
public class Singleton {
// 私有构造函数
private Singleton(){}
// 单例对象
private static Singleton singleton = new Singleton();
// 静态工厂
public static Singleton getInstance(){
return singleton;
}
}
通过私有的构造函数,防止其他处进行new操作,从而创建多个对象,保证一个类只能构建一个对象。
singleton是该类的单例对象,初始值主动构建对象单例对象Singleton。
静态工厂方法getInstance(),通过静态工厂方法,直接返回单例对象。
饿汉模式从类初始化,就创建了单例对象,如果该对象在系统中从未使用过,就浪费了资源,没有实现懒加载的作用。
二、懒汉模式
public class Singleton {
// 私有构造函数
private Singleton(){}
// 单例对象
private static Singleton singleton = null;
// 静态工厂
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
通过私有的构造函数,防止其他处进行new操作,从而创建多个对象,保证一个类只能构建一个对象。
singleton是该类的单例对象,初始值为null,需要创建对象时,在通过静态工厂进行创建。
静态工厂方法getInstance(),通过静态工厂方法,获取单例对象。
相比饿汉式,懒汉式实现了懒加载的作用,当使用到该单例对象时,才会去创建对象,实现按需加载。但是懒汉式存在着多线程的安全问题,在多线程的环境下,当singleton对象还是空时,用可能A,B两个线程同时访问getInstance()方法,并同时进入到 if(singleton == null) 判断语句中,执行了 new Singleton() 操作,这样singleton就被创建了两次。
三、双重检验机制
public class Singleton {
// 私有构造函数
private Singleton(){}
// 单例对象
private static Singleton singleton = null;
// 静态工厂
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
为了防止在多线程环境下,多次构建对象的情况,在new Singleton() 之前添加 synchronized 同步锁,锁住整个对象。
当singleton对象还是空时,A,B两个线程同时访问getInstance()方法,并同时通过 if(singleton == null) 判断语句,只用一个线程获取到同步锁,另一个线程只能等待,当构建单例对象完成后,释放同步锁,另一个线程获取同步锁后,再次判断单例对象是否为空,此时已由上一个线程创建完,单例对象不为空,不通过判断语句。如果不做第二次判断的话,当两个线程都进入了第一次判读,另一个线程获得同步锁后,还会再次创建单例对象。
但是,双重检验机制也非绝对的安全,有可能因为JVM编译器的指令重排的原因,返回一个没有初始化完成的单例对象。
分析:
先来描述A,B两个线程同时执行 getInstance() 方法,A先获得锁,先创建了 singleton 对象,B进入第二次 if(singleton == null) 判断语句,singleton 对象已经不为空,判断不通过,返回 singleton 对象。
上面的流程看似没有任何问题,但是这里面涉及到了JVM编译器的指令重排,情况可能就会有不一样的结果。
假如执行创建对象的语句 singleton = new Singleton(); 会被JVM编译器编译成以下的指令:
1)分配对象的内存空间;
2)初始化对象;
这种情况下的指令顺序,对于上述的流程,是没有问题的,但是,由于经过JVM和CPU的优化,指令有可能重排成下面的顺序:
1)分配对象的内存空间;
2)初始化对象;
当A线程执行完1)3)步时,此时单例对象还未完成初始化,但是已经不为null了,B线程进入到 if(singleton == null) 判断语句,判断结果为false,返回一个还没有初始化的单例对象,从而会引发报错。
我们可以在单例对象前加入一个volatile,防止指令重排。
改进版
public class Singleton {
// 私有构造函数
private Singleton(){}
// 单例对象
private volatile static Singleton singleton = null;
// 静态工厂
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
JMM(内存模型)允许编译器在生成指令顺序的时候,可以插入特定类型的内存屏障来禁止指令重排序。volatile关键字就是内存屏障。当编译器在生成指令顺序的时候,发现了volatile,就直接忽略掉。不再重排序了。当使用volatile关键字修饰后,JVM的指令始终保持以下的顺序:
1)分配对象的内存空间;
2)初始化对象;
四、内部静态类
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在第一次调用getInstance方法时,虚拟机才会加载静态内部类LazyHolder并初始化INSTANCE 。只有一个线程可以获得对象的初始化锁,其他的线程是无法进行对象的初始化,保证对象的唯一性。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
但是,内部静态类的方式,也存在着问题:无法防止反射来重复构建对象。
反射示例:
public class Main {
public static void main(String[] args) {
try {
// 获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
// 设置为可访问
con.setAccessible(true);
// 构造两个不同的对象
Singleton test1 = (Singleton) con.newInstance();
Singleton test2 = (Singleton) con.newInstance();
// 验证是否是不同对象
System.out.println(System.identityHashCode(test1));
System.out.println(System.identityHashCode(test2));
System.out.println(test1.equals(test2));
}catch (Exception e) {
e.printStackTrace();
}
}
}
控制台结果:
1845066581
1018937824
false
从结果上看,毫无疑问,这是两个不同的对象。
五、枚举方式
public enum SingletonEnum {
INSTANCE;
}
使用枚举的方式实现单例模式,可以利用枚举的语法糖,JVM会阻止反射创建对象,而且创建的线程是安全的,任何情况下都是单例的。
枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。
原理:
枚举类隐藏了私有的构造函数
枚举类的域是相应类型的一个实例对象
但是,枚举类实现的单例,并非懒加载,其对象在枚举类加载的时候就被初始化。
使用枚举的方式实现单例模式是《Effective Java》中推荐的单例模式之一,但是在实际项目中比较少使用,主要是因为可读性并不高,而且在项目中,比较少的情况下利用反射去构建单例对象。
©白色的野骆驼
bug与你同在