【java】双重检查加锁到底锁了个啥?
·
在多线程环境下实现单例模式时,双重检查加锁(DCL) 是一种经典写法。它兼顾了线程安全和性能优化,但常常让人困惑:“为什么要这样写?”、“锁的对象应该是什么?”、“为什么不锁 this 或 instance?” 本文将系统梳理这些问题,并重点分析锁对象的本质。
1. 单例与线程安全问题
最朴素的单例写法:
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 线程不安全
}
return instance;
}
}
在多线程环境下,可能多个线程同时进入 if (instance == null),导致重复创建实例。
2. synchronized 的简单方案
直接对方法加锁:
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
虽然线程安全,但每次调用都要加锁,性能很差。
3. 双重检查加锁的思路
优化思路:只在第一次初始化时加锁,后续访问直接返回。
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
4. 锁的本质
在 Java 中:
synchronized (lock) {
// 临界区
}
- 锁的对象:JVM 会加锁的是引用指向的 对象实例本身(堆区对象),而不是引用变量本身;
- 引用只是找到对象的钥匙:访问引用不会加锁,加锁生效的是引用指向的对象;
- 所以所有线程必须使用 同一个对象实例作为锁,才能保证互斥;
- 这也是为什么 DCL 中锁对象通常使用
static final,保证引用唯一且不可修改。
总结一句话:双重检查加锁中,锁的是对象本身,而不是引用;引用只是用来找到这个对象。
5. 锁 class 对象 vs 锁堆区对象
5.1 锁 class 对象
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
特点:
Class对象在 JVM 中全局唯一;- 无需额外创建锁对象;
- 教科书写法,直观。
优势:
- 简洁、天然唯一;
- 所有线程共享同一把锁。
劣势:
- 可能被外部误用,锁粒度过大。
5.2 锁堆区对象
private static final Object lock = new Object();
synchronized (lock) {
if (instance == null) {
instance = new Singleton();
}
}
特点:
new Object()创建对象在堆区,static final引用指向它;- 专用锁对象,语义更清晰。
优势:
- 锁粒度小,封闭在类内部;
- 不容易被外部代码误用。
劣势:
- 稍显啰嗦;
- 如果不是
final,可能被修改,导致锁失效。
5.3 对比总结
| 锁对象类型 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
Singleton.class |
简单、自然、全局唯一 | 可能被误用,锁粒度大 | 常见单例,代码简洁优先 |
| 堆区锁对象 | 锁语义清晰,作用域更小 | 代码稍复杂,非 final 有风险 | 强调锁粒度、避免外部干扰 |
6. 为什么不能锁 this?
- 单例初始化前没有实例,也就没有
this; - 即便已有实例,
this也不是全局唯一,无法保证互斥。
7. 为什么不锁 instance?
-
初始化前
instance为 null,无法加锁; -
初始化完成后,访问安全:
- 引用赋值是原子操作;
volatile保证可见性和禁止重排;
-
锁只保护 初始化过程,而不是访问过程。
8. DCL 的边界
- 解决问题:单例对象只初始化一次,线程安全;
- 不能解决:单例对象方法内部的并发问题,内部共享可变状态仍需额外同步。
9. static final 的必要性
-
static:保证锁对象和类绑定,所有线程共享; -
final:保证引用不可变,不会被指向别的对象; -
不用
static final可能导致:- 实例字段无法使用(实例未创建,锁不存在);
- 非 final 的 static 字段被修改,锁失效,DCL 崩溃。
10. 核心总结
- DCL 锁 对象本身,引用只是找到对象的钥匙;
- 锁的作用:保护 初始化过程,访问过程无需加锁;
Singleton.class:简单天然唯一,粒度大;static final Object lock:专用堆对象,粒度小,语义清晰;- 不能锁
this; - 方法内部线程安全问题需单独处理。
📌 比喻:
DCL 的锁就是“谁有权造工牌”的仲裁器。
工牌造好后,大家直接刷卡用,不需要每次都去找 HR 审批。
更多推荐



所有评论(0)