懒汉式单例模式的线程问题

懒汉式单例模式的线程问题
Liuxz在多线程的情况下 有可能创建出多个实例对象
可以用线程同步的方法去处理 比如把方法声明为synchronized
1 | public class Test3 { |
这段代码实现了单例模式(饿汉式的线程安全版本),
通过 synchronized 关键字保证了 getCatInstance() 方法在多线程环境下的安全性,确保只会创建一个 Cat 实例,因此 c1 == c2 的结果为 true。
这两个单例模式的实现方式有显著区别,核心差异在于同步的范围和效率,具体如下:
1. 第一个实现(双重检查锁 / Double-Checked Locking)
java
运行
1 | public static Cat getCatInstance() { |
核心特点:
仅在
cat为null时才进入同步块,通过两次null检查(无锁时一次,持有锁时一次),既保证了线程安全,又减少了同步带来的性能损耗。- 第一次检查:避免已初始化后仍进入同步块(多数情况下直接返回,无需加锁)。
- 第二次检查:防止多线程同时通过第一次检查后,重复初始化(持有锁时再确认)。
优势:
同步范围小(仅初始化时加锁),后续调用无需竞争锁,效率更高,适合高并发场景。
2. 第二个实现(方法级同步)
java
运行
1 | public synchronized static Cat getCatInstance() { |
核心特点:
直接在静态方法上使用
synchronized,相当于对Cat.class加锁(与第一个实现的同步块锁对象相同),但每次调用方法都会触发同步。问题:
即使
cat已初始化(单例已创建),后续所有调用仍需竞争锁并排队执行,导致不必要的性能开销(尤其是高并发场景下)。
总结区别
| 维度 | 双重检查锁(第一个) | 方法级同步(第二个) |
|---|---|---|
| 同步范围 | 仅初始化时的同步块 | 整个方法(每次调用都同步) |
| 性能 | 高(后续调用无锁) | 低(每次调用都需竞争锁) |
| 线程安全 | 安全(需配合volatile修饰cat) |
安全(但代价高) |
| 适用场景 | 高并发、频繁调用的单例 | 低并发、对性能不敏感的场景 |
注意
第一个实现需额外用volatile修饰cat变量(private static volatile Cat cat;),否则可能因指令重排序导致线程安全问题(可见性和有序性)。而第二个实现因方法级同步,无需volatile也能保证安全,但性能较差。
实际开发中,双重检查锁是更优的单例实现方式(需正确使用volatile)。
volatile
这是什么
volatile 是 Java 中的一个关键字,主要用于修饰变量,核心作用是保证变量的 可见性 和 有序性,但不保证原子性。它常被用于多线程场景,解决线程间变量读取不一致或指令重排序导致的问题(比如之前提到的双重检查锁单例)。
一、先搞懂:没有 volatile 会出什么问题?
在多线程环境中,CPU、编译器和 JVM 为了优化性能,可能会做一些 “手脚”,导致变量操作不符合预期:
- 缓存导致的可见性问题:每个线程有自己的工作内存(缓存),对共享变量的修改会先存在工作内存,不会立即同步到主内存;其他线程读取时也从自己的缓存取,导致看不到最新值。
- 指令重排序导致的有序性问题:编译器 / CPU 会调整指令执行顺序(只要不影响单线程结果),但多线程下可能破坏逻辑正确性(比如双重检查锁中的
cat = new Cat()被重排序)。
二、volatile 的核心作用
1. 保证可见性
当一个线程修改了 volatile 修饰的变量后,这个修改会立即同步到主内存;其他线程读取该变量时,会直接从主内存读取最新值,而不是从自己的工作缓存读取过期数据。
举个例子:
java
运行
1 | // 无 volatile:线程A修改flag后,线程B可能长期看不到,陷入死循环 |
2. 禁止指令重排序
volatile 会禁止编译器和 CPU 对该变量相关的指令进行重排序,保证指令按代码书写的顺序执行,避免多线程下的逻辑混乱。
关键场景:双重检查锁单例
之前的双重检查锁实现中,cat = new Cat() 看似是一步操作,实际会被拆分为 3 步:
- 分配内存空间;
- 初始化 Cat 对象;
- 将 cat 引用指向内存空间。
没有 volatile 时,CPU 可能重排序为 1→3→2。此时线程 A 执行到步骤 3(cat 已非 null),线程 B 第一次检查发现 cat 不为 null,直接返回一个未初始化完成的 Cat 对象,导致错误。
用 volatile 修饰 cat 后,会禁止这种重排序,确保 1→2→3 执行,避免半初始化对象问题。
三、volatile 的局限性:不保证原子性
volatile 无法保证复合操作的原子性(原子性指操作要么全部完成,要么全部不执行,中间不会被打断)。
举个反例:
java
运行
1 | private static volatile int count = 0; |
最终 count 的结果很可能小于 10000。因为 count++ 是 3 步复合操作,volatile 只能保证每次读取的是最新值,但无法阻止多个线程同时执行 “读取 - 加 1 - 写入” 的步骤,导致值被覆盖。
解决原子性问题:需配合锁(synchronized)或原子类(AtomicInteger)。
四、volatile 的使用场景
- 状态标志位:比如多线程中的开关变量(如上面的 flag),用于线程间传递状态。
- 双重检查锁单例:修饰单例变量,禁止指令重排序,避免半初始化对象。
- 轻量级同步:适用于变量的操作是 “单次读 / 写”(非复合操作)的场景,替代
synchronized以提升性能。
五、总结
| 特性 | volatile 是否保证? |
|---|---|
| 可见性 | 是 |
| 有序性 | 是(禁止重排序) |
| 原子性 | 否(仅单次读写安全) |
volatile 是一种轻量级的多线程同步方案,性能优于 synchronized,但适用场景有限。核心记住它的两大作用:解决可见性问题、禁止指令重排序,同时注意它不处理原子性问题。




