synchronized 锁升级过程是怎样的?
2026年01月13日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,通常意在考察以下几个层面的理解:
- 对 Java 锁机制和
synchronized底层实现的深入理解:不仅仅是知道synchronized用于加锁,更想知道你是否了解其背后为了平衡性能而设计的复杂优化机制。 - 对 JVM 运行时数据区(尤其是对象头)的掌握:锁升级的信息存储在何处?这是理解升级过程的基础。
- 对多线程竞争场景下性能权衡的思考:为何要设计多级锁?无锁、偏向锁、轻量级锁、重量级锁各自解决了什么问题,又带来了什么开销?
- 知识的时效性与深度:是否了解从历史版本到当前主流版本(如 JDK 15+)的演变,特别是偏向锁的争议与默认状态变化。
核心答案
synchronized 的锁升级是 JVM 为了在无竞争或低竞争场景下减少性能开销,而在高竞争场景下保障稳定性的优化过程。其升级路线是:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。这个过程是不可逆的。
在主流 JDK 版本中(如 JDK 15 及以后),偏向锁默认已被禁用,因此经典的升级路径通常简化为:无锁 -> 轻量级锁 -> 重量级锁。
深度解析
原理/机制
- 存储位置:锁状态标志存储在 Java 对象的对象头的 Mark Word 中。
- 无锁:一个新创建的对象,尚未有任何线程竞争。
- 偏向锁 (Biased Locking):
- 目的:消除在无实际竞争情况下,同一个线程重入锁的同步开销(如
Vector的大部分方法)。 - 机制:当一个线程第一次访问同步块时,会在对象头和栈帧锁记录中存储偏向的线程 ID。之后该线程再进入和退出同步块时,无需进行 CAS 加解锁,只需简单检查 Mark Word 中的线程 ID 是否为自己。
- 升级触发:一旦有另一个线程尝试获取这个锁,偏向模式立即宣告结束,锁会升级为轻量级锁。
- 现状:由于维护成本和收益问题,自 JDK 15 起,偏向锁默认已被禁用且未来可能移除。
- 目的:消除在无实际竞争情况下,同一个线程重入锁的同步开销(如
- 轻量级锁 (Lightweight Lock):
- 目的:在线程交替执行、没有真正并发竞争的同步块中,避免直接使用重量级锁(操作系统互斥量)带来的性能消耗。
- 机制:线程在执行同步块前,JVM 会先在当前线程的栈帧中创建锁记录空间,并将对象头的 Mark Word 复制到锁记录中(称为 Displaced Mark Word)。然后,线程尝试通过 CAS 操作将对象头的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁。如果失败,表示存在竞争(另一个线程也进行了同样的 CAS 操作),锁会膨胀为重量级锁。
- 重量级锁 (Heavyweight Lock):
- 目的:应对高并发、高强度竞争的场景。
- 机制:此时锁对象的 Mark Word 指向一个监视器对象 (Monitor, 即 ObjectMonitor) 的指针。未获得锁的线程会被阻塞,并进入该监视器的等待队列,等待操作系统的调度(如挂起、唤醒)。这涉及到用户态到内核态的切换,开销最大。
代码示例
以下代码的同步块,在运行时可能会经历锁升级。
public class LockUpgradeExample {
private final Object lock = new Object();
private int counter = 0;
public void increment() {
synchronized (lock) { // 锁对象`lock`
counter++;
}
}
}
- 线程 A 首次调用
increment():若偏向锁开启,lock对象会偏向线程 A(偏向锁阶段)。 - 线程 B 也调用
increment():偏向锁撤销,升级为轻量级锁,线程 B 通过 CAS 自旋尝试获取锁。 - 若线程 B 自旋等待一段时间后(或自旋次数超过阈值)仍未获得锁,或又有线程 C 加入竞争:锁膨胀为重量级锁,线程 B、C 被阻塞,进入等待队列。
对比分析与注意事项
- 重量级锁的两种实现:在 Linux 上,早期的重量级锁使用互斥量 (pthread_mutex_t) 实现;现代的优化(如
synchronized)可能使用更高效的 Park/Unpark 机制配合队列管理,但其本质仍然是会引发线程阻塞和调度的重量级操作。 - 自旋与自适应自旋:轻量级锁竞争时,线程不会立即阻塞,而是进行 “自旋”(空循环等待)。JVM 采用了自适应自旋,即根据之前自旋的成功历史动态调整自旋时间,以避免无谓的 CPU 浪费。
- 锁消除与锁粗化:锁升级是 “锁优化” 的一种。JVM 还会进行其他优化,如逃逸分析后的锁消除(如果发现锁对象不可能被共享,则移除同步操作),以及相邻同步块的锁粗化以减少锁的获取/释放次数。
最佳实践与常见误区
- 误区1:认为
synchronized在任何情况下都慢。经过锁升级和一系列优化后,它在低竞争场景下性能与显式锁(如ReentrantLock)相差无几,甚至更优(因为 JVM 能进行更多底层优化)。 - 误区2:过度关注偏向锁细节。对于使用 JDK 15+ 的开发者,应更关注轻量级锁与重量级锁的机制。
- 最佳实践:理解升级机制有助于我们写出更合理的同步代码。例如,避免在长时间持有锁的代码块中执行耗时操作(如 I/O),这极易引发锁膨胀为重量级锁,导致系统吞吐量急剧下降。对于明确的、可控的高并发场景,可以考虑使用
java.util.concurrent包下更灵活的显式锁。
总结
synchronized的锁升级是一个 “按需逐步加码” 的优化策略,其核心思想是在保证线程安全的前提下,根据实际的竞争激烈程度,选择开销最小的同步方式。理解这一过程,有助于我们深入理解 JVM 并发优化思想,并更好地进行高并发程序设计。