设计模式之单例模式
顾名思义,单例模式就是用来保证一个类只能构建一个对象的设计模式。
初级版本
一个初始版本的单例模式实现如下:构建方法是私有的,只能由类内部调用;单例对象只能通过 getInstance 方法获取,不能直接访问。
public class Singleton {
private static Singleton instance = null; // 单例对象
private Singleton() {} // 私有构造函数
// 静态工厂方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
如果单例初始值是null,还未构建,则构建单例对象并返回。这个写法属于单例模式当中的懒汉模式。
如果单例对象一开始就被new Singleton()主动构建,则不再需要判空操作,这种写法属于饿汉模式。
同步锁保护版本
单例模式的初级版本不是线程安全的。当 instance 对象为空,同时有两个线程访问 getInstance 方法时,因为 instance 为空,所以两个线程都调用的 new 操作。这样一来,instance 明显就被构建了两次。因此,这里我们加一个同步锁进行保护。
同步锁保护的线程安全版本(双重检测机制):在判空之前加上 Synchronized 同步锁锁住整个类(而不是使用对象锁),防止 new 操作执行多次;进入 Synchronized 临界区后,两个线程都已经通过第一次判空,此时实例对象有可能已经由另一个线程创建,因此当前还要再进行一次判空。
public class Singleton {
private static Singleton instance = null; // 单例对象
private Singleton() {} // 私有构造函数
// 静态工厂方法
public static Singleton getInstance() {
if (instance == null) { // 基本检测
synchronized (Singleton.class) { // 同步锁
if (instance == null) { // 双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
volatile 线程安全版本
但是上述使用同步锁和双重检测机制的版本依然不是绝对的线程安全,仍然存在漏洞。这里涉及到 JVM 编译器的指令重排。
顾名思义,指令重排是指 JVM 或者 CPU 为了优化程序的执行过程,将程序运行时的指令重新排序,导致指令执行的顺序发生变更,进而导致在某个时刻程序所处的状态也会不同。举个例子,在 Java 中执行 instance = new Singleton 时,编译出来的 JVM 指令为:
memory = allocate(); // 分配内存空间
ctorInstance(memory); // 初始化对象
instance = memory; // 实现对地址的引用
然而这些指令有可能在经过 JVM 或者 CPU 的优化后,重排成一下的顺序:
memory = allocate(); // 分配内存空间
instance = memory; // 实现对地址的引用
ctorInstance(memory); // 初始化对象
如果指令重排后,一个线程运行到第二步,实现了对地址的引用,但并没有完成初始化,而此时另一个线程抢占到 CPU 资源,进行判空会得到 false 结果,进而返回一个没有完成初始化的 instance 对象。
为了避免这一情况,我们需要在 instance 对象前增加一个 修饰符 volatile。
public class Singleton {
private volatile static Singleton instance = null; // 单例对象
private Singleton() {} // 私有构造函数
// 静态工厂方法
public static Singleton getInstance() {
if (instance == null) { // 基本检测
synchronized (Singleton.class){ // 同步锁
if (instance == null) { // 双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
根据 volatile 的定义,volatile 关键字声明了一个变量的值有可能在不同访问操作时发生改变,它避免了由于编译器对序列读写的优化从而导致的重读旧值或者省略写操作。也就是说,volatile 阻止了变量访问前后的指令重排,保证了指令的执行顺序。同时,volatile 可以保证线程访问的变量值是主内存中的最新值。
经过 volatile 的修饰,当线程在进行判空时,得到的 instance 要么为空,要么就是一个初始化完成的对象,不会再出现某个中间态,从而保证了线程安全。
静态内部类
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
静态内部类实现单例模式的方法十分巧妙,内部静态类和实例构建方法都是私有的,从外部无法进行访问。在 Singleton 类被加载的时候,instance 并没有开始初始化,而是当 getInstance 方法被调用时,使得静态内部类开始被加载,从而完成 instance 的对象初始化。
这里采用了类加载(classloader)的加载模式来实现懒加载,保证了构建单例对象的安全性。
利用反射重复构建对象
//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));
枚举类
使用枚举类来实现单例非常优雅和简洁,同时还能阻止反射的构建方法,还能保证线程安全,从而保证构建单个对象的安全性。
public enum SingletonEnum {
INSTANCE;
}
枚举类实现单例模式利用了 enum 的语法属性,因为 JVM 会阻止反射获取枚举类的私有构建方法。
不过枚举类也有它的缺点,它并非使用懒加载,其实例对象在枚举类被加载时就完成初始化了。
简单总结:
单例模式 | 线程安全 | 懒加载 | 防止反射构建 |
---|---|---|---|
双重锁检测 | 是 | 是 | 否 |
静态内部类 | 是 | 是 | 否 |
枚举类 | 是 | 否 | 是 |