synchronized关键字底层实现
约 1905 字大约 6 分钟
javasynchronized
2025-03-04
概述
synchronized 是Java中最基本的同步机制,通过Monitor(监视器/管程)对象实现互斥和协作。Java 6 之后引入了偏向锁、轻量级锁等优化,使得 synchronized 在无竞争或轻竞争场景下的性能大幅提升,与 ReentrantLock 的差距已经非常小。
使用方式
public class SynchronizedDemo {
private final Object lock = new Object();
private int count;
// 1. 同步实例方法 — 锁this
public synchronized void increment() {
count++;
}
// 2. 同步静态方法 — 锁Class对象
public static synchronized void staticMethod() {
// ...
}
// 3. 同步代码块 — 锁指定对象
public void blockSync() {
synchronized (lock) {
count++;
}
}
}Monitor机制
每个Java对象都关联一个Monitor(ObjectMonitor),Monitor是 synchronized 实现的核心。
monitorenter / monitorexit 字节码
// 同步代码块编译后的字节码
public void blockSync();
Code:
0: aload_0
1: getfield #3 // lock
4: dup
5: astore_1
6: monitorenter // 获取锁
7: aload_0
8: dup
9: getfield #2 // count
12: iconst_1
13: iadd
14: putfield #2
17: aload_1
18: monitorexit // 正常释放锁
19: goto 27
22: astore_2
23: aload_1
24: monitorexit // 异常释放锁(保证锁一定被释放)
25: aload_2
26: athrow
27: return
Exception table:
from to target type
7 19 22 any
22 25 22 any编译器自动插入两条 monitorexit:一条用于正常退出,一条用于异常退出,确保锁一定被释放。
同步方法则不使用 monitorenter/monitorexit,而是在方法的 access_flags 中设置 ACC_SYNCHRONIZED 标志,JVM 在方法调用时隐式获取和释放Monitor。
对象头(Mark Word)结构
synchronized 的锁状态信息存储在对象头的 Mark Word 中。以64位JVM为例:
|-------------------------------------------------------|-------|
| Mark Word (64 bits) | State |
|-------------------------------------------------------|-------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | 0 | 01 | 无锁
|-------------------------------------------------------|-------|
| thread_id:54 | epoch:2 | unused:1 | age:4 | 1 | 01 | 偏向锁
|-------------------------------------------------------|-------|
| ptr_to_lock_record:62 | 00 | 轻量级锁
|-------------------------------------------------------|-------|
| ptr_to_heavyweight_monitor:62 | 10 | 重量级锁
|-------------------------------------------------------|-------|
| empty | 11 | GC标记
|-------------------------------------------------------|-------|锁升级过程
偏向锁(Biased Locking)
适用于只有一个线程反复获取同一把锁的场景,是最轻量的锁状态。
偏向锁的核心优势:重入时 零开销,只需比较线程ID。
注意:JDK 15 默认禁用偏向锁(
-XX:-UseBiasedLocking),JDK 18 完全移除。因为偏向锁撤销需要 stop-the-world,在现代多核环境下得不偿失。
轻量级锁
当第二个线程尝试获取偏向锁时,偏向锁升级为轻量级锁。
// 概念性描述轻量级锁获取过程
// 1. 在当前线程栈帧中创建 Lock Record(锁记录)
// 2. 将 Mark Word 复制到 Lock Record(Displaced Mark Word)
// 3. CAS 将 Mark Word 替换为指向 Lock Record 的指针
// 4. CAS 成功 → 获取轻量级锁
// 5. CAS 失败 → 自旋重试或升级为重量级锁重量级锁
自旋失败后,锁膨胀为重量级锁。此时 Mark Word 指向一个 ObjectMonitor 对象,竞争线程进入 _EntryList 阻塞等待(用户态 → 内核态切换)。
自适应自旋
JVM 会根据上一次自旋的结果动态调整自旋次数:
- 上次自旋成功 → 增加自旋次数
- 上次自旋失败 → 减少自旋次数,甚至跳过自旋直接阻塞
wait / notify / notifyAll
这三个方法必须在 synchronized 块中调用,它们操作的是当前对象的 Monitor。
public class ProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 10;
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // 释放锁,进入WaitSet
}
queue.offer(item);
notifyAll(); // 唤醒WaitSet中的线程到EntryList
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
int item = queue.poll();
notifyAll();
return item;
}
}锁消除与锁粗化
锁消除(Lock Elimination)
JIT编译器通过逃逸分析,发现锁对象不会逃逸出线程时,会消除锁操作。
// JIT可能消除此处的锁
public String concat(String s1, String s2) {
StringBuffer sb = new StringBuffer(); // sb不逃逸
sb.append(s1); // StringBuffer的append是synchronized的
sb.append(s2); // 但sb是局部变量,JIT会消除锁
return sb.toString();
}锁粗化(Lock Coarsening)
连续对同一对象加锁/解锁时,JIT 会将多次锁操作合并为一次。
// 优化前:循环内反复加锁解锁
for (int i = 0; i < 100; i++) {
synchronized (lock) {
// ...
}
}
// JIT优化后(概念性):锁粗化
synchronized (lock) {
for (int i = 0; i < 100; i++) {
// ...
}
}synchronized vs ReentrantLock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM内置 | JDK(AQS) |
| 释放方式 | 自动(离开作用域) | 手动(try-finally) |
| 可中断获取 | 不支持 | lockInterruptibly() |
| 超时获取 | 不支持 | tryLock(timeout) |
| 公平锁 | 不支持 | 支持 |
| 条件变量 | 单一(wait/notify) | 多个(Condition) |
| 锁升级 | 偏向→轻量→重量 | 无 |
| 性能 | JDK 6+ 优化后接近 | 高并发下略优 |
常见FAQ
Q: synchronized可重入吗?
A: 是的。同一线程可以多次获取同一把锁,Monitor 通过 _recursions 计数器记录重入次数。每次 monitorenter 加1,monitorexit 减1,减到0时释放锁。
Q: synchronized能保证可见性吗?
A: 是的。monitorexit 时会将工作内存中的修改刷新到主内存,monitorenter 时会从主内存重新加载。这由 JMM 的 happens-before 规则保证。
Q: 为什么不建议用 String 对象作为锁?
A: 字符串常量池导致不同代码块可能使用同一个 String 对象作为锁,产生意外的互斥。应使用 new Object() 或 private final Object lock = new Object()。
总结
synchronized 从一个"重量级"关键字,经过 JDK 6 的锁升级优化、JIT 的锁消除和锁粗化,已经成为一个高效的同步工具。理解其底层的 Monitor 机制、Mark Word 结构和锁升级路径,有助于在实际开发中做出合理的同步方案选择。对于简单的互斥场景,优先使用 synchronized;需要更灵活的控制(超时、可中断、公平)时,使用 ReentrantLock。
贡献者
更新日志
9f6c2-feat: organize wiki content and refresh site setup于