锁策略、锁升级、锁消除和锁粗化
- 锁策略
-
- [悲观锁 vs 乐观锁](#悲观锁 vs 乐观锁)
- [公平锁 vs 非公平锁](#公平锁 vs 非公平锁)
- [可重入锁 vs 不可重入锁](#可重入锁 vs 不可重入锁)
- [自旋锁 vs 挂起等待锁](#自旋锁 vs 挂起等待锁)
- [互斥锁 vs 读写锁](#互斥锁 vs 读写锁)
- [轻量级锁 vs 重量级锁](#轻量级锁 vs 重量级锁)
- 总结⭐
- 锁升级
-
- [synchronized 锁自动升级路径⭐](#synchronized 锁自动升级路径⭐)
-
- 对象头与锁状态
- [无锁 → 偏向锁(Biased Locking)](#无锁 → 偏向锁(Biased Locking))
- [偏向锁 → 轻量级锁](#偏向锁 → 轻量级锁)
- [轻量级锁→ 重量级锁](#轻量级锁→ 重量级锁)
- 锁消除
- 锁粗化
本文将深入理解Java中的常见锁策略 ,并重点探讨JVM层面的三大优化手段:锁升级(Lock Escalation)、锁消除(Lock Elimination)与锁粗化(Lock Coarsening)。
锁策略
从不同的维度看,锁可以分为多种类型:
悲观锁 vs 乐观锁
-
悲观锁 :总是假设最坏的情况------每次读写数据,别人都会来修改。所以它会在操作前先加锁,阻塞其他线程。
-
乐观锁 :很天真地认为冲突一般不会发生,所以先不加锁,直接操作。更新时,再检查一下数据有没有被别人动过。如果没被改,就写入;如果被改了,就重试或放弃。
synchronized和ReentrantLock都是典型的悲观锁。
适用场景 :乐观锁适合读多写少 的场景,能减少加锁开销;悲观锁则适合写操作频繁的场景,避免无休止的重试。
公平锁 vs 非公平锁
多线程排队等锁,锁被释放时,该轮到谁?
- 公平锁:严格遵循先来后到。线程A比B先来排队,A就一定能比B先拿到锁。
- 非公平锁:不排队。锁一释放,所有等待的线程(甚至刚来的新线程)一起哄抢,谁抢到算谁的。
synchronized就是典型的非公平锁。ReentrantLock则支持通过构造参数自由选择是公平还是非公平。
公平锁虽然看起来更公平,但它进行线程调度和维护等待队列的成本更高。非公平锁性能更好,但可能导致某些线程始终抢不到锁,造成饥饿。
可重入锁 vs 不可重入锁
一个已经拿到锁的线程,还能再拿一次这把锁吗?
- 可重入锁:允许。同一个线程可以多次获取同一把锁,不会自己把自己锁死。比如一个同步方法里调用另一个同步方法。
- 不可重入锁 :不允许。线程第二次获取锁时会阻塞,直到自己释放,但这永远不可能发生,于是造成死锁。
synchronized和ReentrantLock都是可重入锁。
可重入锁的完整实现逻辑
自旋锁 vs 挂起等待锁
当线程抢锁失败,是原地等待还是暂时放弃CPU?
-
自旋锁(Spin Lock) :抢锁失败的线程不放弃CPU资源,而是原地死循环 ,反复尝试获取锁,直到成功。
优点 :一旦锁被释放,自己能瞬间感知并获取,没有线程调度的延迟。
缺点:如果锁被持有很久,自旋的线程会空耗CPU,造成浪费。 -
挂起等待锁 :线程抢锁失败后,直接进入阻塞状态,让出CPU资源。等锁释放后,系统再重新调度唤醒它。
优点 :不浪费CPU资源,线程阻塞期间CPU可以去做更有意义的事。
缺点:从阻塞到被唤醒,存在调度延迟。
互斥锁 vs 读写锁
-
互斥锁是最简单也最严格的锁模式。它的规则只有一条: 任何时刻,只能有一个线程持有锁,无论是读还是写。
线程A(读)🔒 ──────────── 🔓 线程B(读) ⏳等待 🔒 ──── 🔓 线程C(写) ⏳等待 ⏳等待 🔒 ──── 🔓 ───────────────────────────────────────────→ 时间 -
读写锁(ReadWriteLock):读写锁把读和写区别对待,引入了三种状态:
无锁状态 :没有任何线程持有锁。
读锁(共享锁) :多个线程可以同时 持有,彼此不阻塞。
写锁(独占锁):一次只能有一个线程持有,且与其他所有锁互斥。线程A(读)🔒共享 ──────────── 🔓 线程B(读)🔒共享 ──────────── 🔓 线程C(写) ⏳等待 🔒独占 ──── 🔓 ───────────────────────────────────────────→ 时间
实际规则表:
| 当前锁状态 | 申请读锁 | 申请写锁 |
|---|---|---|
| 无锁 | ✅ 获得读锁 | ✅ 获得写锁 |
| 已被读锁持有 | ✅ 可重入/共享 | ❌ 阻塞 |
| 已被写锁持有 | ❌ 阻塞 | ✅ 仅持有线程可重入 |
在Java中,核心实现是 ReentrantReadWriteLock:
java
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock(); // 共享锁
Lock writeLock = rwLock.writeLock(); // 独占锁
// 读操作:多个线程可同时执行
public String readData() {
readLock.lock();
try {
return sharedData;
} finally {
readLock.unlock();
}
}
// 写操作:独占执行
public void writeData(String newVal) {
writeLock.lock();
try {
sharedData = newVal;
} finally {
writeLock.unlock();
}
}
适用场景 :读多写少。比如配置缓存、元数据读取等,用读写锁能让大量读线程并发执行,性能远高于互斥锁。
轻量级锁 vs 重量级锁
| 对比维度 | 轻量级锁 | 重量级锁 |
|---|---|---|
| 等待方式 | 自旋等待(忙等,占用CPU) | 挂起等待(释放CPU,进入阻塞队列) |
| 实现层级 | JVM层面,用户态CAS操作 | 操作系统层面,内核态Mutex |
| 适用场景 | 锁持有时间短、竞争不激烈 | 锁持有时间长、竞争激烈 |
| 加锁开销 | 小(只是一条CPU原子指令) | 大(系统调用,用户态↔内核态切换) |
| 等待开销 | 空转消耗CPU | 不消耗CPU,但线程切换开销大 |
| 线程状态 | 线程始终处于 RUNNABLE 状态 | 线程进入 BLOCKED 状态 |
| 锁记录位置 | 线程栈帧中的 Lock Record | 堆中对象关联的 ObjectMonitor |
| Java中的定位 | synchronized 的低竞争优化形态 |
synchronized 的最终兜底形态 |
两种锁的工作流程
设线程切换开销 为
S,锁持有时间 为T。
- 如果 T < S :自旋的好处(不切换)大于好处(避免CPU空转) → 选轻量级锁/自旋
- 如果 T > S :CPU空转的消耗大于切换的节省 → 选重量级锁/挂起
具体场景举例:
| 场景 | 锁持有时间 | 推荐锁类型 |
|---|---|---|
给计数器 i++ 加锁 |
几纳秒 | 轻量级锁(自旋) |
| 写入一个大文件或网络IO | 几百毫秒 | 重量级锁(挂起) |
| 保护一段简单赋值 | 极短 | 无锁CAS更好 |
总结⭐
| 锁策略 | 核心问题 | 关键特性 / 适用场景 |
|---|---|---|
| 乐观锁 vs 悲观锁 | 冲突概率多大? | 读多写少用乐观,写多用悲观 |
| 公平锁 vs 非公平锁 | 锁该按什么顺序给? | 需要公平可配置,追求性能用非公平 |
| 可重入 vs 不可重入 | 我能重复加这个锁吗? | Java的锁基本都是可重入的 |
| 自旋锁 vs 挂起等待 | 等锁时CPU让不让? | 临界区短用自旋,临界区长用挂起 |
| 读写锁 | 读和写能拆开管吗? | 读多写少场景的终极优化利器 |
| 轻量级 vs 重量级 | 加锁代价多大? | 竞争少用轻量,竞争多升级重量 |
锁升级
synchronized 锁自动升级路径⭐
为了解决重量级锁(挂起等待)带来的内核态切换开销,JDK 6引入了偏向锁 和轻量级锁 ,synchronized的锁状态会随着竞争情况逐步升级,且不可降级。
锁升级路径为:无锁 → 偏向锁 → 轻量级锁 → 重量级锁 。
synchronized 锁自动升级路径_可视化

简单说明
无锁
└─ 一个线程来了 → 偏向锁(记录线程 ID,不加锁就来)
└─ 另一个线程也来了 → 轻量级锁(CAS 自旋,原地等待)
└─ 自旋太久抢不到 → 重量级锁(系统互斥量,线程挂起排队)
对象头与锁状态
JVM通过对象头中的Mark Word来记录锁状态。不同状态下Mark Word的存储内容不同:
| 锁状态 | 标志位 | 偏向位 | 存储内容说明 |
|---|---|---|---|
| 无锁 | 01 | 0 | 对象哈希码、分代年龄 |
| 偏向锁 | 01 | 1 | 持有锁的线程ID、偏向时间戳 |
| 轻量级锁 | 00 | - | 指向栈中锁记录(Lock Record)的指针 |
| 重量级锁 | 10 | - | 指向操作系统互斥量(Monitor)的指针 |
无锁 → 偏向锁(Biased Locking)
- 思想:大多数时候,锁总是由同一个线程多次获取。JVM会偏向于第一个获取锁的线程。
- 过程:当线程T1首次访问同步块时,JVM通过CAS将T1的线程ID写入对象头。之后T1再次进入同步块时,无需任何同步操作,直接执行。
- 撤销:当线程T2尝试竞争锁时,JVM会暂停T1,检查T1是否仍在执行同步块。若已退出则撤销偏向锁;若仍在执行则升级为轻量级锁。
- 注意:从JDK 15开始,偏向锁特性被标记为废弃,因为它在高并发场景下的维护成本(如撤销时的STW)甚至高于收益。
偏向锁 → 轻量级锁
- 思想:多个线程虽然是竞争关系,但往往是交替执行,即"几乎没有实际竞争"。
- 过程 :线程在进入同步块前,在栈帧中创建锁记录(Lock Record),将Mark Word复制到锁记录中,然后通过CAS自旋尝试将对象头中的Mark Word替换为指向锁记录的指针。
- 竞争失败 :如果自旋等待后仍未获得锁,说明竞争加剧,锁膨胀为重量级锁。
轻量级锁→ 重量级锁
- 机制:依赖操作系统底层的互斥量(Mutex)实现。未获取到锁的线程不再自旋,而是进入阻塞态,等待被唤醒。
- 代价:涉及系统调用和线程上下文切换,CPU开销大,但在高竞争场景下能保证系统吞吐量。
锁消除
锁消除(Lock Elimination) 是一项编译器优化技术。JIT编译器在动态编译同步块时,如果通过逃逸分析(Escape Analysis) 发现锁对象只被一个线程访问(即没有逃逸出当前线程),就会认为该锁不存在竞争,从而直接移除掉锁的申请与释放逻辑。
典型场景:
在方法内部使用StringBuffer(线程安全,方法加锁)或Vector时,如果该对象是局部变量且未被其他线程引用,JIT就会大方地去掉锁。
java
// 优化前:看似每次append都要加锁
public String buildString() {
StringBuffer sb = new StringBuffer(); // 局部变量,无逃逸
sb.append("Hello");
sb.append(" World");
return sb.toString();
}
// 优化后:JVM实际执行的效果相当于使用了无锁的StringBuilder
这项优化让我们不必过度担心使用线程安全类带来的性能损耗,只要作用域未逃逸,JVM会智能处理。
锁粗化
与锁消除相反,锁粗化(Lock Coarsening) 解决的是锁操作过于零碎的问题。如果JIT检测到在一段代码中,相邻的多个同步块反复使用同一个锁对象,它会将这些零散的锁合并成一个范围更大的同步块。
典型场景:循环体内的加锁
java
// 优化前:每次循环都加锁、解锁
for(int i = 0; i < 1000; i++) {
synchronized(this) {
doSomething(); // 简单操作
}
}
// 优化后:JVM将锁扩展到循环外部
synchronized(this) {
for(int i = 0; i < 1000; i++) {
doSomething();
}
}
这样做虽然增大了单个线程的锁持有时间,但显著减少了加锁和解锁的次数,从而节省了CPU开销。

