在 Java 中,synchronized 是解决多线程并发问题的关键机制,它的底层实现和锁的升降级机制涉及 JVM 和操作系统的协作。下面我们从底层原理到实际应用,逐步拆解这个复杂但重要的机制。


一、synchronized 的底层实现

1. 对象头与 Mark Word

每个 Java 对象在内存中分为三部分:对象头(Header)实例数据(Instance Data)对齐填充(Padding)
synchronized 的锁信息存储在对象头的 Mark Word 中(32位/64位结构不同,以32位为例):

锁状态Mark Word(32位)内容
无锁25位哈希码 + 4位分代年龄 + 1位偏向锁标志(0) + 2位锁标志(01)
偏向锁23位线程ID + 2位Epoch + 4位分代年龄 + 1位偏向锁标志(1) + 2位锁标志(01)
轻量级锁指向栈中锁记录的指针(30位) + 2位锁标志(00)
重量级锁指向操作系统互斥量(mutex)的指针(30位) + 2位锁标志(10)
2. 锁的升级流程

synchronized 的锁状态会随着竞争情况逐步升级,目的是减少直接使用重量级锁的开销。流程如下:

首次进入同步块
有其他线程竞争
竞争加剧
无锁
偏向锁
轻量级锁
重量级锁
(1)偏向锁(Biased Locking)
  • 场景:适用于单线程重复进入同步块的场景(如循环内加锁)。
  • 原理:Mark Word 中记录线程ID,后续该线程进入同步块时无需CAS操作。
  • 优化意义:消除无竞争时的同步开销(连 CAS 都不需要)。
  • 触发条件:默认启用(JVM 参数 -XX:+UseBiasedLocking)。
(2)轻量级锁(Thin Lock)
  • 场景低竞争环境(多线程交替执行,但未同时争抢锁)。
  • 原理
    1. 线程在栈帧中创建锁记录(Lock Record)
    2. 通过 CAS 将 Mark Word 替换为指向锁记录的指针。
    3. 若成功,线程获得锁;若失败,说明有竞争,升级为重量级锁。
  • 优化意义:避免直接调用操作系统互斥量,减少内核态切换。
(3)重量级锁(Heavyweight Lock)
  • 场景高竞争环境(多线程同时争抢锁)。
  • 原理:依赖操作系统的 互斥量(mutex)条件变量,未抢到锁的线程进入阻塞状态。
  • 开销:涉及用户态到内核态的切换,性能最差。
3. 锁的降级
  • 降级条件:在 HotSpot JVM 中,重量级锁不会降级到轻量级锁(设计选择,因降级带来的收益不如升级明显)。
  • 特殊场景:在 G1 垃圾回收器 的某些阶段(如并发标记),会强制所有偏向锁撤销为无锁状态。

二、锁升级的底层细节

1. 偏向锁的获取与撤销
  • 获取
    synchronized(obj) {  // 第一次加锁时,Mark Word 记录线程ID
        // code...
    }
    
  • 撤销:当其他线程尝试获取锁时:
    1. 暂停持有偏向锁的线程(STW)。
    2. 检查原线程是否存活或仍在同步块中:
      • 若已退出同步块,撤销为无锁状态。
      • 若仍在同步块,升级为轻量级锁。
2. 轻量级锁的膨胀

当多个线程通过 CAS 竞争轻量级锁失败时:

  1. JVM 分配一个 重量级锁 所需的 monitor 对象(位于堆中)。
  2. Mark Word 更新为指向 monitor 的指针。
  3. 未抢到锁的线程进入 阻塞队列,等待唤醒。
3. monitor 对象的结构

每个 Java 对象关联一个 monitor(由 C++ 实现),结构如下:

  • Owner:当前持有锁的线程。
  • EntryList:阻塞等待锁的线程队列。
  • WaitSet:调用 wait() 后进入的线程队列。

三、为什么需要锁升级?

锁状态优点缺点适用场景
偏向锁无竞争时零开销撤销时有 STW 开销单线程重复访问
轻量级锁避免内核态切换自旋消耗 CPU低竞争、短时间同步
重量级锁解决高竞争问题性能最差(内核切换)高竞争、长时间同步

锁升级的核心目标:在无竞争或低竞争时,避免直接使用重量级锁的开销。


四、实战验证锁升级

1. 查看对象头工具

使用 JOL(Java Object Layout)工具打印对象头信息:

// 添加依赖:org.openjdk.jol:jol-core
public class LockUpgradeDemo {
    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        synchronized (obj) {
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
    }
}

输出示例(片段):

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION        VALUE
      0     4   (object header)     01 00 00 00  # 无锁状态(01)
...

加锁后:

      0     4   (object header)     05 f0 00 00  # 偏向锁(01 + 偏向标志1)
2. 强制触发锁升级
public class HeavyLockDemo {
    public static void main(String[] args) {
        Object lock = new Object();
        
        // 模拟高竞争
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                synchronized (lock) {
                    try { Thread.sleep(100); } 
                    catch (InterruptedException e) {}
                }
            }).start();
        }
        
        // 打印锁状态
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
}

输出将显示锁状态变为 重量级锁(10)


五、常见误区与注意事项

  1. 锁升级不可逆:重量级锁不会降级(某些 JVM 可能例外)。
  2. 偏向锁延迟:JVM 默认在启动后 4 秒才启用偏向锁(-XX:BiasedLockingStartupDelay=0 可禁用延迟)。
  3. 锁消除优化:JIT 编译器可能直接去掉无竞争的锁(如局部变量的锁)。
  4. 锁粗化:连续对同一对象加锁可能合并为一个更大的锁。

总结

synchronized 的底层实现是 Java 并发优化的经典案例:

  1. 锁升级:通过偏向锁、轻量级锁、重量级锁的逐步升级,平衡性能与安全性。
  2. Mark Word:对象头中的元数据动态存储锁状态。
  3. 适用场景:低竞争时偏向锁和轻量级锁大幅提升性能,高竞争时重量级锁保证正确性。

理解这些机制,能帮助你更好地编写高效、线程安全的代码。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐