【自存】懒汉式单例模式中的多线程经典问题
单例模式
单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例。这一点在很多场景上都需要。比如 JDBC 中的 DataSource 实例就只需要一个。单例模式的具体实现方法又分为饿汉和懒汉两种:
- 饿汉:指的是在创建一个类的时候就将实例创建好!比较急!
- 懒汉:指的是在需要用到实例的时候再去创建实例!比较懒!
因为我们单例模式只能有一个实例,那如何去保证一个实例呢?我们会马上想到类中用 static 修饰的类属性,它只有一份!保证了单例模式的基本条件!
情况一:饿汉
class Singleton{private static Singleton instance = new Singleton();private Singleton(){}public static Singleton getInstance() {return instance;}
}
我们可以看到这里饿汉模式,当多个线程并发时,并不会出现线程不安全问题,因为这里的设计模式只是针对了读操作!而单例模式的更改操作,需要看懒汉模式!
情况二:懒汉
实际情况是,只有当首次调用单例时在进行创建:
class Singleton1{private static Singleton1 instance = null;private Singleton1(){}public static Singleton1 getInstance() {if(instance==null){//①:需要时再创建实例!instance = new Singleton1();//②}return instance;}
}
❗ 存在的问题:可能创建多个实例!
假设两个线程 A 和 B 同时调用 getInstance()
。
- 两者都执行到 ①,发现 instance == null → 都进入 if 块。
- 两者都执行 ② → 各自 new 了一个对象!
- ❌ 违反了“单例”的核心原则:全局唯一实例。
情况三:简单加锁
针对上面的情况,一个很直观的思路是:给读写操作进行加锁。所有线程在进入这个 synchronized (Singleton.class) 代码块前,必须先获取这个 Class 对象的内置锁(monitor)。同一时刻,只有一个线程能持有这个锁,其他线程必须等待。
class Singleton2{private static Singleton2 instance = null;private Singleton2(){}public static Singleton2 getInstance() {synchronized (Singleton.class){ //对读写操作进行加锁!if(instance==null){//需要时再创建实例!instance = new Singleton2();}return instance;}}
}
等价于
public static synchronized Singleton2 getInstance() {if(instance==null){//需要时再创建实例!instance = new Singleton2();}return instance;
}
❗ 仍然存在的问题:所有调用都要加锁!
- 即使 instance 已经初始化完成,每次读取调用
getInstance()
仍要竞争锁。 - 单例对象创建后是“只读”的,后续读取完全没必要加锁!
- ❌ 高并发场景下成为性能瓶颈。
情况四:双重检查锁
针对上面问题,引入双重检查锁:双重检查锁(DCL)的精髓在于:
- 第一次检查(无锁) → 如果对象已创建,直接返回,完全不加锁,性能极高!
- 只有在对象未创建时,才加锁 + 第二次检查。
class Singleton2{private static Singleton2 instance = null;private Singleton2(){}public static Singleton2 getInstance() {if(instance==null){//如果未初始化就进行加锁操作!synchronized (Singleton.class){ //对读写操作进行加锁!if(instance==null){//需要时再创建实例!instance = new Singleton2();}}}return instance;}
}
❗ 但是!这段代码仍然有问题:
⚠️ 核心问题:JVM 的指令重排序导致其他线程可能拿到未初始化的对象!
如前所述,instance = new Singleton2();
在底层分为三步:
- 分配内存
- 初始化对象(构造方法)
- instance 引用指向内存地址
JVM 可能重排序为:1 → 3 → 2
👉 线程 A 执行到第 3 步(instance != null),但对象还没初始化(第 2 步没执行)
👉 线程 B 跳过第一个 if(因为 instance != null),直接 return instance
👉 线程 B 拿到的是半初始化对象 → 调用方法或访问字段时可能崩溃!
问题:第二if
是否可以删掉?
答:不可以,我们发现当有多个线程进行了第一个 if 判断后,进入的线程中有一个线程锁竞争拿到了锁,而其他线程就在这阻塞等待,直到该锁释放后,又有线程拿到了该锁,如果没有 if 判断,新拿到锁的线程又会执行实例创建代码,这样也就多次创建了实例,显然不可!!!
情况五:终极解决方案
加上 volatile
关键字, volatile 的作用:
- 禁止指令重排序 → 保证“初始化完成”再赋值给 instance。
- 保证可见性 → 一个线程修改 instance,其他线程立即可见。
class Singleton2{private static volatile Singleton2 instance = null;private Singleton2(){}public static Singleton2 getInstance() {if(instance==null){//如果未初始化就进行加锁操作!synchronized (Singleton.class){ //对读写操作进行加锁!if(instance==null){instance = new Singleton2();}}}return instance;}
}