深入解析Java synchronized底层原理与锁升级机制

引言

在 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 同步代码块:monitorentermonitorexit

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 对象在内存中由三部分组成:

  1. 对象头(Object Header)

  2. 实例数据(Instance Data)

  3. 对齐填充(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:

  1. 检查 _count :若 _count == 0,表示锁未被占用,将 _owner 设为当前线程,_count 加 1

  2. 可重入 :若 _count > 0_owner 是当前线程,_count 加 1(即可重入)

  3. 阻塞等待 :若 _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 自旋尝试获取锁,避免线程阻塞和上下文切换。

实现方式

  1. 线程在栈帧中创建一个锁记录(Lock Record)

  2. 将对象头的 Mark Word 复制到锁记录中

  3. 通过 CAS 将对象头的 Mark Word 替换为指向锁记录的指针

  4. 若 CAS 成功,获取轻量级锁

  5. 若 CAS 失败,说明存在竞争,线程进入自旋状态,不断重试

自旋锁:线程在没有获得锁时不进入阻塞,而是循环检测锁是否被释放。自旋会占用 CPU,因此适用于锁持有时间短的场景。

自适应自旋锁 :自旋次数不是固定的,而是根据前一次在同一锁上的自旋结果动态调整。如果上次自旋成功,本次允许更多次自旋;反之则减少自旋次数,以最大化资源利用。

5.4 重量级锁(Heavyweight Locking)

升级条件:当锁被同一个线程持有时间过长,或其他线程自旋次数超过阈值仍无法获取锁时,轻量级锁膨胀为重量级锁。

实现方式 :JVM 使用操作系统提供的互斥量(Mutex) 实现。未获取到锁的线程被挂起(阻塞) ,不再占用 CPU,等待操作系统调度唤醒。

性能代价 :重量级锁涉及用户态与内核态的切换以及线程的阻塞与唤醒,开销较大。

六、JVM 的锁优化技术

6.1 锁消除(Lock Elimination)

JVM 通过逃逸分析 判断一段同步代码是否不可能存在多线程竞争 。如果确认不存在竞争,JVM 会直接消除这个锁,从而避免不必要的加锁解锁开销。

6.2 锁粗化(Lock Coarsening)

如果一段连续的代码对同一个对象 频繁进行加锁和解锁,JVM 会将多个锁合并为一次加锁,即扩大锁的范围到整个操作序列,减少加锁解锁的次数。

七、synchronizedReentrantLock 的对比

特性 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 管理线程队列与锁状态

  • 性能优化层面 :通过偏向锁 → 轻量级锁 → 重量级锁的渐进式升级机制,在无竞争、低竞争和高竞争场景下分别采用最优策略

理解这些底层原理,不仅有助于写出更高效的并发代码,更能在面对死锁、性能瓶颈等复杂问题时,具备从根源分析和解决问题的能力。