引言
在 Java 并发编程中,synchronized 关键字是最基础也是最常用的同步机制。它由 JVM 原生支持,能够保证多线程环境下共享资源的原子性 、可见性 与有序性 。然而,许多开发者对 synchronized 的理解仅停留在使用层面,对其底层实现原理缺乏系统认知,这在生产环境出现死锁、性能瓶颈或线程安全问题时,往往导致排查困难。
本文将从字节码实现、对象内存布局、Monitor 机制、锁升级流程及 JVM 优化等多个维度,深入剖析 synchronized 的底层原理。
一、synchronized 的三种使用范式
synchornized 的锁粒度由锁对象决定,根据使用方式的不同,锁对象也有所区别:
| 使用方式 | 锁对象 | 作用范围 |
|---|---|---|
| 修饰实例方法 | this(当前实例对象) |
同一实例的所有同步方法互斥 |
| 修饰静态方法 | 当前类的 Class 对象 |
该类所有实例的静态同步方法互斥 |
| 修饰代码块 | 括号内指定的对象 | 同一锁对象的所有同步代码块互斥 |
java
public class SynchronizedDemo {
// 1. 修饰实例方法:锁当前实例对象
public synchronized void instanceMethod() { /* ... */ }
// 2. 修饰静态方法:锁当前类的 Class 对象
public static synchronized void staticMethod() { /* ... */ }
// 3. 修饰代码块:指定锁对象
public void blockMethod() {
synchronized (this) { /* ... */ }
synchronized (SynchronizedDemo.class) { /* ... */ }
}
}
二、字节码层面的实现
2.1 同步代码块:monitorenter 与 monitorexit
当 synchronized 修饰代码块时,编译器会在字节码中插入两条关键指令:
-
monitorenter:插入在同步代码块的起始位置,尝试获取对象监视器(Monitor)的所有权 -
monitorexit:插入在同步代码块的正常结束和异常抛出路径,释放对象监视器的所有权
值得注意的是,编译器会生成两个 monitorexit 指令------一个对应正常执行路径,一个对应异常执行路径,确保无论代码是否抛出异常,锁都能被正确释放,避免死锁。
2.2 同步方法:ACC_SYNCHRONIZED 标志
当 synchronized 修饰方法时,字节码中并不插入 monitorenter/monitorexit 指令,而是在方法的访问标志(access_flags)中设置 ACC_SYNCHRONIZED 标记。
JVM 在调用方法时,会检查该方法是否携带 ACC_SYNCHRONIZED 标志。若携带,则自动获取对应对象的 Monitor,方法执行完成后自动释放。
无论是 monitorenter/monitorexit 指令还是 ACC_SYNCHRONIZED 标志,其底层本质都是获取对象关联的 Monitor 锁。
三、对象头与 Mark Word:锁信息的载体
3.1 对象的内存布局
在 HotSpot JVM 中,Java 对象在内存中由三部分组成:
-
对象头(Object Header)
-
实例数据(Instance Data)
-
对齐填充(Padding)
其中,对象头 是 synchronized 锁机制的核心载体。在 32 位 HotSpot 虚拟机中,对象头包含:
-
Mark Word(32 bit):存储对象的哈希码、GC 分代年龄、锁状态标志等
-
Klass Pointer(32 bit):指向对象所属类的元数据指针
3.2 Mark Word 的状态变化
Mark Word 是一个动态的数据结构,会根据对象的锁状态复用存储空间。不同锁状态下 Mark Word 的存储内容如下:

| 锁状态 | 存储内容 | 标志位 |
|---|---|---|
| 无锁 | 对象哈希码 + GC 分代年龄 | 01 |
| 偏向锁 | 线程 ID + GC 分代年龄 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 |
| 重量级锁 | 指向 Monitor 对象的指针 | 10 |
Mark Word 的最后两位是锁标志位,JVM 通过读取这两个比特位即可判断当前对象的锁状态。
四、Monitor 机制:重量级锁的核心
4.1 什么是 Monitor
Monitor 被翻译为监视器 或管程 ,是操作系统提供的同步原语。JVM 规定,每一个 Java 对象都有一个与之关联的 Monitor 对象。
在 HotSpot JVM 中,Monitor 由 C++ 实现的 ObjectMonitor 结构体表示,其核心字段包括:
cpp
ObjectMonitor {
_owner; // 指向当前持有锁的线程
_count; // 锁计数器(支持可重入性)
_recursions; // 重入次数
_entryList; // 入口队列(竞争锁失败的线程)
_waitSet; // 等待队列(调用 wait() 的线程)
}
4.2 Monitor 的加锁与解锁流程
当一个线程执行 monitorenter 指令时,会尝试获取对象关联的 Monitor:
-
检查
_count:若_count == 0,表示锁未被占用,将_owner设为当前线程,_count加 1 -
可重入 :若
_count > 0且_owner是当前线程,_count加 1(即可重入) -
阻塞等待 :若
_count > 0且_owner不是当前线程,当前线程进入_entryList阻塞等待
执行 monitorexit 时,_count 减 1。当 _count 减到 0 时,释放锁并唤醒 _entryList 中的等待线程。
当持有 Monitor 的线程调用 wait() 方法时,会释放锁并将自己放入 _waitSet 中等待被唤醒。
五、锁升级机制:从轻量到重量的优化之路
5.1 为什么需要锁升级
在 JDK 1.6 之前,synchronized 是重量级锁,每次加锁都直接调用操作系统的互斥量(Mutex Lock),涉及用户态与内核态的切换,开销巨大。
然而在大多数实际场景中,锁竞争并不激烈,甚至很多同步代码块始终由同一个线程反复执行。如果每次都使用重量级锁,会造成严重的性能浪费。
为此,JDK 1.6 引入了锁升级机制 ,JVM 会根据锁的竞争激烈程度,动态地将锁状态从无锁 → 偏向锁 → 轻量级锁 → 重量级锁 进行不可逆的升级。
5.2 偏向锁(Biased Locking)
核心思想:如果一个锁被同一个线程反复获取,就让这把锁"偏向"这个线程,后续该线程再次进入同步块时无需任何同步操作。
实现方式 :当第一个线程尝试加锁时,JVM 通过一次 CAS 操作将对象头 Mark Word 中的线程 ID 设置为当前线程,并将锁标志位设为 01(偏向锁)。
撤销时机:当另一个线程尝试获取已被偏向的锁时,触发偏向锁撤销。JVM 会检查偏向的线程是否仍然存活:
-
若已死亡,将锁恢复为无锁状态,允许新线程通过 CAS 重新偏向
-
若仍存活,在安全点检查该线程是否仍在同步块中
-
若已释放锁,恢复为无锁状态
-
若仍持有锁,升级为轻量级锁
-
批量重偏向与批量撤销:频繁的偏向锁撤销开销较大,JVM 引入了批量重偏向和批量撤销机制作为优化。
5.3 轻量级锁(Lightweight Locking)
核心思想 :当存在少量竞争时,通过 CAS 自旋尝试获取锁,避免线程阻塞和上下文切换。
实现方式:
-
线程在栈帧中创建一个锁记录(Lock Record)
-
将对象头的 Mark Word 复制到锁记录中
-
通过 CAS 将对象头的 Mark Word 替换为指向锁记录的指针
-
若 CAS 成功,获取轻量级锁
-
若 CAS 失败,说明存在竞争,线程进入自旋状态,不断重试
自旋锁:线程在没有获得锁时不进入阻塞,而是循环检测锁是否被释放。自旋会占用 CPU,因此适用于锁持有时间短的场景。
自适应自旋锁 :自旋次数不是固定的,而是根据前一次在同一锁上的自旋结果动态调整。如果上次自旋成功,本次允许更多次自旋;反之则减少自旋次数,以最大化资源利用。
5.4 重量级锁(Heavyweight Locking)
升级条件:当锁被同一个线程持有时间过长,或其他线程自旋次数超过阈值仍无法获取锁时,轻量级锁膨胀为重量级锁。
实现方式 :JVM 使用操作系统提供的互斥量(Mutex) 实现。未获取到锁的线程被挂起(阻塞) ,不再占用 CPU,等待操作系统调度唤醒。
性能代价 :重量级锁涉及用户态与内核态的切换以及线程的阻塞与唤醒,开销较大。
六、JVM 的锁优化技术
6.1 锁消除(Lock Elimination)
JVM 通过逃逸分析 判断一段同步代码是否不可能存在多线程竞争 。如果确认不存在竞争,JVM 会直接消除这个锁,从而避免不必要的加锁解锁开销。
6.2 锁粗化(Lock Coarsening)
如果一段连续的代码对同一个对象 频繁进行加锁和解锁,JVM 会将多个锁合并为一次加锁,即扩大锁的范围到整个操作序列,减少加锁解锁的次数。
七、synchronized 与 ReentrantLock 的对比
| 特性 | synchronized |
ReentrantLock |
|---|---|---|
| 实现层级 | JVM 内置(C++ 实现) | Java 层实现(基于 AQS) |
| 锁获取方式 | 自动获取/释放 | 需手动 lock() / unlock() |
| 可重入性 | 支持 | 支持 |
| 中断响应 | 不可中断 | 支持 lockInterruptibly() |
| 公平性 | 非公平锁 | 支持公平/非公平 |
| 性能(JDK 1.6+) | 低竞争场景优异 | 高竞争场景更灵活 |
在 JDK 1.6 引入偏向锁、轻量级锁等优化后,synchronized 的性能在大多数场景下已与 ReentrantLock 相差无几。官方甚至建议在两种方式都可选的情况下优先使用 synchronized。
八、总结
synchornized 作为 Java 最基础的同步机制,其底层实现经过 JDK 1.6 的深度优化后,已经从早期的"重量级锁"演变为一个根据竞争程度自适应的智能锁系统:
-
字节码层面 :通过
monitorenter/monitorexit指令或ACC_SYNCHRONIZED标志实现 -
存储层面 :依赖对象头的 Mark Word 存储锁状态信息
-
Monitor 层面 :由
ObjectMonitor管理线程队列与锁状态 -
性能优化层面 :通过偏向锁 → 轻量级锁 → 重量级锁的渐进式升级机制,在无竞争、低竞争和高竞争场景下分别采用最优策略
理解这些底层原理,不仅有助于写出更高效的并发代码,更能在面对死锁、性能瓶颈等复杂问题时,具备从根源分析和解决问题的能力。