锁优化与升级源码分析
synchronized 在 JDK 6 之后经过大量优化,从最初的"重量级锁"演进为一条无锁 → 偏向锁 → 轻量级锁 → 重量级锁的升级路径。理解这条路径,才能解释"为什么 synchronized 性能不差"以及"什么时候该换 Lock"。
锁的四种状态
锁对象的状态记录在对象头的 Mark Word 中,从低到高依次为:
无锁(01)→ 偏向锁(01+偏向位)→ 轻量级锁(00)→ 重量级锁(10)各状态 Mark Word 的布局:
| 状态 | Mark Word 内容 |
|---|---|
| 无锁 | 对象哈希、分代年龄 |
| 偏向锁 | 线程 ID、epoch、分代年龄 |
| 轻量级锁 | 指向栈中锁记录的指针 |
| 重量级锁 | 指向 Monitor 的指针 |
锁可以升级但不能降级(偏向锁可被批量撤销,但一般视为不降级)。设计目标:多数场景无竞争或低竞争,用最轻的机制解决。
偏向锁
原理
第一个获取锁的线程,通过 CAS 把自己的线程 ID 写入对象头 Mark Word。此后该线程再次进入同步块时,不需要任何同步操作,直接判断对象头中线程 ID 是否是自己。
线程 A 首次进入:CAS 将线程 ID 写入 Mark Word → 偏向 A
线程 A 再次进入:检查线程 ID == A → 直接进入(无锁操作)撤销
偏向锁不会主动释放。当有第二个线程竞争时:
1. 到达安全点(Safe Point)
2. 持有偏向锁的线程被暂停
3. 检查原线程是否仍持有:已退出 → 偏向位清零恢复无锁;未退出 → 升级为轻量级锁
4. 原线程恢复执行偏向锁在竞争激烈时反而增加撤销开销,可通过 -XX:-UseBiasedLocking 关闭(JDK 15 起默认禁用偏向锁,JDK 18 起移除)。
轻量级锁
获取
竞争出现时,偏向锁升级为轻量级锁:
1. 线程在自己的栈帧中分配一个锁记录(Lock Record)
2. 将对象头的 Mark Word 复制到锁记录中(Displaced Mark Word)
3. CAS 尝试把对象头 Mark Word 更新为指向锁记录的指针
4. CAS 成功 → 获取轻量级锁;失败 → 说明有竞争,膨胀为重量级锁释放
1. 用 CAS 把锁记录中的 Displaced Mark Word 写回对象头
2. 成功 → 释放完成
3. 失败 → 说明锁已膨胀为重量级锁,释放时需唤醒等待线程轻量级锁利用 CAS 避免重量级锁的线程阻塞/唤醒开销,适合"无竞争但需要同步"的场景。CAS 失败即膨胀,不会自旋等待。
重量级锁
原理
竞争升级后,锁对象关联一个 Monitor(管程),Mark Word 指向 Monitor 地址。Monitor 内部维护:
Monitor(ObjectMonitor)
├── _owner 持有锁的线程
├── _WaitSet 调用 wait() 的线程队列
├── _EntryList 等待获取锁的线程队列
└── _recursions 可重入计数重量级锁的获取/释放涉及 OS 层面的互斥量与线程阻塞/唤醒,存在用户态/内核态切换开销,所以被称为"重量级"。
优化:自旋锁与自适应自旋
为了减少阻塞开销,重量级锁获取前先自旋(忙等一小段时间):
- 自旋:短暂循环尝试获取锁,避免立即阻塞(-XX:+UseSpinning)
- 自适应自旋(JDK 6+):自旋次数不固定,由前一次在同一个锁上的自旋时间与锁持有者的状态动态决定。若前一次自旋成功,下次允许更多自旋;若经常失败,则减少甚至不自旋
锁消除与锁粗化
这两个是 JIT 编译器在编译期的优化,与运行期的锁状态升级不同。
锁消除
编译器通过逃逸分析发现:同步块中的对象不会逃逸出线程,锁没有意义,直接删除。
public String concat(String a, String b) {
StringBuffer sb = new StringBuffer(); // sb 未逃逸,是线程私有的
sb.append(a); // append 是 synchronized 方法
sb.append(b); // 锁消除后,这些同步全部移除
return sb.toString();
}StringBuffer 的 append 是同步方法,但 sb 只在方法内使用,未逃逸,JIT 会消除这些锁。这也是编译器提示"StringBuilder 更快但 StringBuffer 被优化后差别不大"的原因之一。
锁粗化
连续的多次加锁/解锁(如循环内每次迭代都加锁)开销很大,编译器把多个细粒度锁合并为一个粗粒度锁:
synchronized (this) { ... } // 连续三次加解锁
synchronized (this) { ... }
synchronized (this) { ... }
// 粗化为:
synchronized (this) {
... ...
... ...
... ...
}锁粗化是性能正收益的优化:减少锁的获取/释放次数。它与"缩小锁粒度"的编码建议不冲突——编码建议针对业务逻辑,锁粗化针对编译后的字节码。
锁升级流程图
线程 A 首次进入
│
▼
┌─── 偏向锁(CAS 写入线程 ID,无竞争零开销)
│ │ 线程 B 竞争
│ ▼
│ 轻量级锁(CAS 复制 Mark Word,自旋无阻塞)
│ │ CAS 失败(竞争加剧)
│ ▼
└─── 重量级锁(Monitor + 阻塞/唤醒,OS 级互斥)常见面试点
- synchronized 锁一定很慢吗? 不是。无竞争时偏向锁/轻量级锁开销极小;竞争激烈时才膨胀为重量级锁。
- 锁可以降级吗? 规范上只升级不降级;偏向锁可被批量撤销(如大量线程交替访问同一锁时)。
- 轻量级锁与重量级锁怎么选? 锁持有的临界区短、竞争少 → 轻量级锁(自旋替代阻塞);临界区长、竞争激烈 → 重量级锁更合适(自旋浪费 CPU)。
- 偏向锁为什么被移除? 偏向锁在 JDK 15 默认禁用、JDK 18 移除。原因是现代 JVM 与应用场景中,偏向锁的撤销与维护开销在多数场景下大于收益,且与新一代垃圾回收器(ZGC 等)的兼容成本高。