单例模式的五种实现方式
参考 https://blog.csdn.net/qq_42372017/article/details/121192445
1. 单例模式的四大原则
- 构造方法私有
- 以静态方法或者枚举返回实例
- 多线程环境下都是访问同一个实例
- 反序列化时不会重新构建对象
2. 单例模式通常有5种实现方法
2-1 饿汉模式
饿汉模式会在类初始化时就提前创建了对象,是一种以空间换取时间的方法,所以不存在线程安全问题。(记忆方法:一个人很饿,那他就会提前把食物准备好)
public class SingleTon {
// 提前实例化
private static SingleTon INSTANCE = new SingleTon();
// 构造方法私有
private SingleTon(){}
public static SingleTon getInstance() {
return INSTANCE;
}
}
2-2 懒汉模式
懒汉模式会在方法被调用的时候才去创建对象,以时间换取空间,在多线程下存在线程安全问题,当多个线程调用getInstance()方法时可能都看到INSTANCE = null,从而导致多个线程去创建对象,违背单例模式的原则。
public class SingleTon {
private static SingleTon INSTANCE = null;
private SingleTon() {}
public static SingleTon getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingleTon();
}
return INSTANCE;
}
}
2-3 双重锁懒汉模式(DCL)
DCL模式是对懒汉模式的改进,第一次判断 INSTANCE == null是为了避免非必要加锁,只有在第一次加载的时候才会对Class对象进行加锁再实例化,这样既可以节约内存空间,又能保证线程安全。
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;
}
}
但是因为 JVM 存在指令重排的功能,DCL也会存在线程不安全的情况。因为INSTANCE = new SingleTon();
实例化对象看似只有一行代码,但其实在 JVM 中是分为三步执行的:
- 在堆内存中开辟内存空间;
- 在分配的内存空间中实例化SingleTon的各个参数;
- 将对象指向堆内存空间。
由于 JVM 的指令重排原因,所以有可能2还没执行就先执行了3,如果此时再被切换到线程B上,由于执行了3,INSTANCE != null
已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。
具体解决方案是使用 volatile 关键字,可以防止指令重排,从而解决DCL失效问题:private volatile static SingleTon INSTANCE = null;
2-4 静态内部类模式(常用)
使用静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不用去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHolder类,只有当获取INSTANCE实例,也就是getInstance()方法第一次被调用时,才会导致虚拟机加载SingleTonHolder类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
缺点:由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,只能调用固定的构造函数。
public class SingleTon {
private SingleTon() {}
private static class SingleTonHolder {
private static SingleTon INSTANCE = new SingleTon();
}
public static SingleTon getInstance() {
return SingleTonHolder.INSTANCE;
}
}
2-5 枚举模式(《Effective Java》推荐)
在《effective java》第83 条:慎用延迟初始化(这本书真的很棒)中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。
public enum SingleTon {
INSTACE;
public void doSomething() {
System.out.println("doSomething");
}
}
// 调用方法:
class Main {
public static void main(String[] args) {
SingleTon.INSTACE.doSomething();
}
}