【Java】synchronized关键字详解:从字节码到对象头与锁升级

【Java】synchronized 关键字详解:从字节码到对象头与锁升级

以下是关于 Java synchronized 关键字 的深度详解(以 2026 年初主流 Java 生态为基准,基于 Java 21 LTS 和 HotSpot JVM 实现)。synchronized 是 Java 并发编程的核心,用于实现线程同步,确保共享资源的互斥访问。它基于 监视器(Monitor) 机制,底层涉及 JVM 的对象头、字节码指令和锁优化。本文从原理到实战,逐层剖析,适合面试准备和实际开发。内容基于 OpenJDK 源码和官方文档整理。

1. synchronized 概述

  • 作用:实现线程同步,防止多线程并发访问共享资源导致的数据不一致。支持方法级和代码块级。
  • 使用场景:多线程环境下保护临界区,如计数器、共享列表。
  • 三种形式
  1. 实例方法public synchronized void method() {}(锁当前对象实例)。
  2. 静态方法public static synchronized void staticMethod() {}(锁 Class 对象)。
  3. 代码块synchronized(obj) {}(锁指定对象 obj)。
  • 特性
  • 可重入:同一个线程可多次获取同一把锁(避免死锁)。
  • 互斥:同一时刻只有一个线程持有锁。
  • 公平性:默认非公平(抢占式),但底层可通过参数调整。
  • 与 ReentrantLock 对比
    维度 synchronized ReentrantLock
    实现 JVM 内置 JDK 类(AQS 框架)
    灵活性 简单,无需手动解锁 支持公平锁、tryLock、interrupt
    性能 JVM 优化后相当 稍高(用户态)
    场景 简单同步 复杂场景 注意:synchronized 在 Java 6+ 后进行了大量优化(如锁升级),性能已接近 ReentrantLock。 2. 从字节码层面剖析 synchronized synchronized 在编译后转换为 monitorentermonitorexit 指令(JVM 规范定义)。这些指令对应监视器的进入和退出。 2.1 示例代码 public class SyncDemo { private final Object lock = new Object(); public void syncMethod() { synchronized (lock) { System.out.println("Hello synchronized"); } } } 2.2 反编译字节码(使用 javap -v SyncDemo.class) 字节码片段(简化版,实际输出更详细): public void syncMethod(); Code: 0: aload_0 1: getfield #2 // Field lock:Ljava/lang/Object; 4: dup 5: astore_1 6: monitorenter // 进入监视器(获取锁) 7: getstatic #3 // System.out 10: ldc #4 // "Hello synchronized" 12: invokevirtual #5 // println 15: aload_1 16: monitorexit // 退出监视器(释放锁) 19: goto 27 22: astore_2 23: aload_1 24: monitorexit // 异常时也需释放锁(JVM 自动生成第二个 monitorexit) 27: return Exception table: from to target type 7 17 22 any 22 25 22 any
    • monitorenter:尝试获取监视器锁。如果锁计数器为 0,则获取成功并加 1(可重入)。失败则阻塞。
    • monitorexit:释放锁,计数器减 1。若减至 0,则完全释放。
    • 异常处理:JVM 自动生成第二个 monitorexit,确保异常时释放锁(防止死锁)。
    • 面试点:为什么有两个 monitorexit?(一个正常退出,一个异常退出)。
    字节码优化:在 Java 8+,JIT 编译器可进一步优化为无锁操作(如偏向锁)。 3. 对象头(Object Header)与锁状态 HotSpot JVM 中,每个对象都有一个 对象头(Mark Word + Class Pointer + Array Length),用于存储锁信息。对象头大小:32 位机 8 字节,64 位机 12/16 字节(压缩指针影响)。 3.1 对象头结构(64 位 HotSpot JVM,启用指针压缩)
    • Mark Word(8 字节):存储 hashCode、GC 分代年龄、锁状态等。
    • Class Pointer(4 字节):指向类元数据。
    • Array Length(可选 4 字节):数组对象才有。
    3.2 Mark Word 中的锁状态位 Mark Word 的最低 2-3 位表示锁状态(Big Endian 存储): 锁状态 Mark Word 结构(64 位) 最低位标志 无锁(Normal) unused:25 | hashCode:31 | unused:1 | age:4 | biased_lock:0 | lock:01 01 偏向锁(Biased) thread_id:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:01 101 轻量级锁(Lightweight) ptr_to_lock_record:62 | lock:00 00 重量级锁(Heavyweight) ptr_to_monitor:62 | lock:10 10 GC 标记 unused:62 | lock:11 11
    • thread_id:持有偏向锁的线程 ID。
    • ptr_to_lock_record:指向线程栈中 Lock Record 的指针(轻量级锁)。
    • ptr_to_monitor:指向 Monitor 对象的指针(重量级锁,基于 ObjectMonitor 实现)。
    • biased_lock:是否启用偏向锁(默认 1,JVM 参数 -XX:BiasedLockingStartupDelay=0 可立即启用)。
    查看对象头工具:使用 OpenJDK 的 jol(Java Object Layout)库: import org.openjdk.jol.info.ClassLayout; Object obj = new Object(); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); // 输出对象头布局
    • 面试点:对象头如何存储 hashCode?(无锁/偏向锁时直接存;升级后移到 Monitor 中)。
    4. 锁升级过程(Lock Escalation) synchronized 默认从无锁开始,根据竞争逐步升级(不可逆,优化性能)。升级由 JVM 内部自旋/阻塞决定,受参数影响(如 -XX:+UseBiasedLocking)。 4.1 升级流程图
    1. 无锁(Unlocked):无线程竞争。Mark Word 存储 hashCode 等。
    2. 偏向锁(Biased Lock)
    • 场景:单线程反复访问。
    • 过程:第一次进入时,CAS 将线程 ID 写入 Mark Word。后续同线程直接检查 ID,无需 CAS。
    • 开销:极低(纳秒级)。
    • 升级触发:其他线程竞争 → 撤销偏向(批量偏向/撤销优化)。
    1. 轻量级锁(Lightweight Lock / Spin Lock)
    • 场景:轻微竞争,线程交替访问。
    • 过程:竞争线程在栈中分配 Lock Record,CAS 将 Mark Word 指向它。自旋等待(默认 10 次,-XX:PreBlockSpin)。
    • 开销:低(自旋消耗 CPU,但避免上下文切换)。
    • 升级触发:自旋失败或竞争加剧 → 膨胀为重量级锁。
    1. 重量级锁(Heavyweight Lock / Monitor Lock)
    • 场景:激烈竞争。
    • 过程:膨胀为 Monitor 对象(C++ 实现),包含 Owner、EntryList(阻塞队列)、WaitSet(等待队列)。使用 OS 互斥锁(pthread_mutex_lock)。
    • 开销:高(上下文切换,微秒/毫秒级)。
    • 释放:唤醒 EntryList 中的线程。
    升级触发条件
    • 偏向 → 轻量级:多线程竞争。
    • 轻量级 → 重量级:自旋超过阈值或线程数 > CPU 核数 / 2。
    • 锁消除/粗化:JIT 优化(如无竞争时移除锁)。
    代码示例(演示升级): public class LockUpgradeDemo { private static final Object lock = new Object(); public static void main(String[] args) { // 无锁/偏向锁 synchronized (lock) { System.out.println("First sync"); // 偏向当前线程 } // 模拟竞争,轻量级/重量级 new Thread(() -> { synchronized (lock) { try { Thread.sleep(100); } catch (Exception e) {} } }).start(); synchronized (lock) { System.out.println("Second sync"); // 可能升级 } } }
    • 监控工具:jstack 查看线程栈(Monitor 信息);JMH 基准测试性能。
    5. 性能优化与最佳实践(2026 视角)
    • 启用/禁用偏向锁:-XX:+UseBiasedLocking(默认开启)。
    • 自旋优化:-XX:AdaptiveSpinning(自适应自旋次数)。
    • 避免问题
    • 锁竞争激烈时,用 ReentrantLock 或分段锁(ConcurrentHashMap)。
    • 不要在循环内加锁(锁粗化)。
    • hashCode() 调用会禁用偏向锁(因为覆盖 Mark Word)。
    • Java 21+ 影响:虚拟线程(Loom)下,synchronized 兼容,但重量级锁可能 pinning(固定载体线程),影响性能。推荐用 ReentrantLock 的虚拟线程模式。
    • 面试高频
    • synchronized 是公平锁吗?(非公平,但可通过队列实现近似公平)。
    • 为什么升级不可逆?(性能考虑,避免频繁切换)。
    • 与 volatile 区别?(volatile 仅可见性,无互斥)。
    synchronized 是 Java 并发基石,理解其底层有助于优化多线程代码。如果需要代码基准测试、JVM 参数调优细节或与 CAS/AQS 对比,继续问~
文章已创建 4206

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部