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。
浙公网安备 33010602011771号