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/
面试考察点
面试官提出这个问题,通常旨在考察以下几个由浅入深的层面:
- 对基础同步机制的理解:你是否清楚
synchronized是实现线程同步的基础关键字。 - 对 JVM 底层实现的掌握:面试官不仅仅想知道 “它能加锁”,更是想考察你是否了解它在 字节码层面 和 JVM 运行时层面 是如何具体实现的,这反映了你的知识深度。
- 对锁优化历史的了解:是否了解从早期 “重量级锁” 到现代 JVM(尤其是 JDK 6 及以后)中一系列 锁升级优化(如偏向锁、轻量级锁)的过程及其意义,这关系到你对性能优化的理解。
- 对比与选型的能力:通过底层实现的讨论,间接考察你与
ReentrantLock等显式锁的对比理解,以及在何种场景下如何选择。 - 实践中的注意事项:是否了解如何正确、高效地使用
synchronized,避免死锁、性能瓶颈等常见问题。
核心答案
synchronized 的实现分为 语法层面 和 运行时层面。
- 语法层面:它通过
monitorenter和monitorexit这一对字节码指令来实现同步代码块的进入和退出;对于同步方法,则通过方法常量池中的ACC_SYNCHRONIZED标志位来标识。 - 运行时层面(核心):JVM 将每个 Java 对象都关联一个 监视器锁(Monitor)。线程通过竞争对象头中的 Mark Word 来获取这个 Monitor。在 JDK 6 之后,为了减少性能开销,引入了 锁升级机制:锁状态会根据竞争情况,从 无锁 -> 偏向锁 -> 轻量级锁(自旋锁) -> 重量级锁 逐步升级。
深度解析
原理/机制
1. 对象头与 Monitor
- 每个 Java 对象在内存中分为三部分:对象头、实例数据、对齐填充。
- 对象头 中的 Mark Word(标记字)是实现锁的关键。它存储了对象的哈希码、分代年龄和 锁状态标志。
- 当锁升级为重量级锁时,Mark Word 中会存储一个指向 操作系统层级互斥量(mutex) 和 等待队列 的指针,这个结构就是 ObjectMonitor(即 Monitor 的具体实现)。
2. 锁升级过程(JDK 6+ 优化后)
这是理解现代 synchronized 性能的关键。
- 偏向锁:假设锁总是由同一线程获得。当一个线程首次进入同步块时,会在对象头和栈帧锁记录中存储偏向的线程 ID。以后该线程再进入时,只需简单检查 ID,无需任何原子操作。适用于近乎无竞争的单线程场景。
- 轻量级锁:当有第二个线程尝试获取锁(发生轻微竞争),偏向锁会升级为轻量级锁。线程会在自己的栈帧中创建锁记录(Lock Record),并尝试通过 CAS 操作 将对象头的 Mark Word 复制到自己的锁记录中,并替换为指向锁记录的指针。如果成功,则获取锁;如果失败,说明存在竞争,会 自旋(循环尝试)一定次数。
- 重量级锁:如果轻量级锁自旋失败(或自旋超过阈值),锁会膨胀为重量级锁。此时,未竞争到锁的线程会被 挂起(Park),进入阻塞状态,等待操作系统调度唤醒。这涉及到用户态到内核态的切换,开销最大。
代码示例
从字节码视角看一个同步块:
// Java 源码
public void syncMethod() {
synchronized(this) {
System.out.println("hello");
}
}
编译后的关键字节码指令如下(可通过 javap -c 查看):
public void syncMethod();
Code:
0: aload_0 // 将对象引用(this)压入操作数栈
1: dup // 复制栈顶值
2: astore_1 // 存储一个副本到局部变量表(用于后续 monitorexit)
5: monitorenter // 尝试获取对象的 Monitor
6: getstatic #2 // 开始执行同步块内的代码
9: ldc #3
11: invokevirtual #4
14: aload_1
15: monitorexit // 正常退出,释放 Monitor
16: goto 24
19: astore_2 // 异常处理开始...
20: aload_1
21: monitorexit // 异常退出,也必须释放 Monitor
22: aload_2
23: athrow
24: return
可以看到,编译器会自动生成配对且确保一定执行的 monitorenter 和 monitorexit 指令,这正是 synchronized 能在发生异常时也释放锁的原因。
对比分析:synchronized vs ReentrantLock
| 特性 | synchronized (隐式锁) | ReentrantLock (显式锁) |
|---|---|---|
| 实现层面 | JVM 层面实现,由 C++ 代码控制 | JDK 层面实现,Java 代码(基于 AQS) |
| 锁的获取 | 自动加锁与释放锁 | 必须手动 lock() 和 unlock(),通常配合 try-finally |
| 灵活性 | 有限。不可中断、非公平(可设公平但不灵活) | 很高。可尝试非阻塞获取(tryLock)、可中断、可设置公平/非公平 |
| 条件队列 | 单一等待队列 (wait/notify) | 可绑定多个 Condition 对象,实现精细的线程等待/唤醒 |
| 性能 | JDK 6 后优化出色,在一般竞争下与 ReentrantLock 持平 | 在高竞争场景下,其可配置性可能带来优势 |
最佳实践与注意事项
- 锁对象选择:应选择 不可变且所有竞争线程都可见 的对象作为锁。通常使用私有静态 final 对象或
this(但需谨慎)。 - 减小锁粒度:尽量只锁必要的代码段(同步块优于同步方法),缩短锁的持有时间。
- 避免死锁:按固定顺序获取多个锁。
- 现代 Java 开发:在竞争不激烈的常规业务代码中,优先使用简洁的
synchronized。只有在需要其不具备的高级特性(如可中断、超时、公平锁、多个条件变量)时,才使用ReentrantLock。
常见误区
- 误区一:
synchronized性能一定很差。纠正:在 JDK 6 引入锁升级后,它在无竞争或低竞争场景下开销极小,性能很好。 - 误区二:锁升级是双向的。纠正:锁升级(膨胀)是 单向 的,一旦升级为重量级锁,就不会再降级回轻量级锁(在某些 JVM 实现中,重量级锁释放后可回到无锁状态,但不会回到偏向/轻量级状态)。
- 误区三:
synchronized锁的是代码。纠正:它锁的是 对象,确切地说是对象的 Monitor。
总结
synchronized 是通过 JVM 层面的 monitorenter/monitorexit 指令和每个对象关联的 Monitor 实现的,并在现代 JVM 中通过 偏向锁 -> 轻量级锁 -> 重量级锁 的升级策略来平衡开销与性能,使其成为多数并发场景下可靠且高效的选择。