单例模式共有四种实现方式:
| 实现方式 | 线程安全 | 懒加载 | 防止反射 |
|---|---|---|---|
| 静态成员属性 | √ | x | x |
| DCL 机制 | √ | √ | x |
| 静态内部类 | √ | √ | x |
| 枚举 | √ | x | √ |
一、静态成员属性
public class Singleton{
private static final Singleton INSTANCE = new Singleton();
private Singleton(){}
public Singleton getInstacne(){
return INSTANCE;
}
}
二、DCL 机制
DCL 又称双重检测锁机制
public class Singleton{
// 注意要加 volatile 防止指令重排序
private volatile Singleton instance;
private Singleton(){}
public Singleton getInstance(){
// 两个 if 即为双重检测锁机制
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
重点:
-
使用 synchronized 锁住 new Singleton() 代码,防止多个线程创建出多个实例
-
if 判断需要在 synchronized 内部。如果在外部,则多线程环境下无法保证只创建一个实例
-
synchronized 外部的 if 其实可以去除,加在这里主要是为了优化性能。如果不在外部做一个 if 判断,那么每个线程 getInstance 时,不管实例是否已经创建,都要等待锁,性能太差。
-
volatile 防止指令重排序。指令重排序是 cpu 在执行汇编指令时为了优化性能,可能会对指令做一个重新排序。比如 new Singleton() 这个代码,有三条指令:
- 分配对象的内存空间
- 初始化对象
- 将 instance 指向刚分配的内存地址
正常情况这三条指令顺序执行,没有问题。但可能经过 JVM 和 CPU 的优化,顺序会变成下面的样子:
- 分配对象的内存空间
- 将 instance 指向刚分配的内存地址
- 初始化对象
在这种情况下,就会出现问题。比如线程1执行到上面的第二步将 instance 指向刚分配的内存地址,此时线程2在进行 if 判断,发现 instance 不为 null 了,就直接返回 instance,但实际上此时 instance 还是一个未初始化的对象,这是线程2在使用该对象时就会出问题。
DCL 机制也可以像下面这样写,但性能很差,并且也不能成为 DCL 机制了,实际中并不建议这样做,这里仅为了方便理解:
public class Singleton{
// 注意要加 volatile 防止指令重排序
private volatile Singleton instance;
private Singleton(){}
public Singleton getInstance(){
// 去除了外层 if,不影响功能,但影响性能
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
return instance;
}
}
三、静态内部类
支持懒加载,弥补了静态成员变量方式的缺点
public class Singleton{
private static class LazyHolder{
private static final INSTANCE = new Singleton();
}
private Singleton(){}
public Singleton getInstance(){
return LazyHolder.INSTANCE;
}
}
该方式利用 JVM 的类加载机制实现懒加载,并且因为是 static 的,也没有线程安全问题。
类加载机制:静态变量仅在被用到时才进行初始化。比如这里 Singleton 被加载并不会导致 LazyHolder.INSTANCE 被加载,只有在调用 Singleton::getInstance 方法时才会去加载 LazyHolder.INSTANCE 变量,以此实现了懒加载
四、枚举
上面三种方式都无法防止用户使用反射来创建实例,而枚举正好可以枚举此缺点。当用户尝试使用反射来创建枚举类的实例时, JVM 会抛出一个错误。
public enum Singleton{
INSTANCE
}
这种方式简单,但却不是懒加载的,枚举类被加载的时候,该单例对象就会被加载。