【Java】synchronized 关键字详解:从字节码到对象头与锁升级
以下是关于 Java synchronized 关键字 的深度详解(以 2026 年初主流 Java 生态为基准,基于 Java 21 LTS 和 HotSpot JVM 实现)。synchronized 是 Java 并发编程的核心,用于实现线程同步,确保共享资源的互斥访问。它基于 监视器(Monitor) 机制,底层涉及 JVM 的对象头、字节码指令和锁优化。本文从原理到实战,逐层剖析,适合面试准备和实际开发。内容基于 OpenJDK 源码和官方文档整理。
1. synchronized 概述
- 作用:实现线程同步,防止多线程并发访问共享资源导致的数据不一致。支持方法级和代码块级。
- 使用场景:多线程环境下保护临界区,如计数器、共享列表。
- 三种形式:
- 实例方法:
public synchronized void method() {}(锁当前对象实例)。 - 静态方法:
public static synchronized void staticMethod() {}(锁 Class 对象)。 - 代码块:
synchronized(obj) {}(锁指定对象 obj)。
- 特性:
- 可重入:同一个线程可多次获取同一把锁(避免死锁)。
- 互斥:同一时刻只有一个线程持有锁。
- 公平性:默认非公平(抢占式),但底层可通过参数调整。
- 与 ReentrantLock 对比:
维度 synchronized ReentrantLock
实现 JVM 内置 JDK 类(AQS 框架)
灵活性 简单,无需手动解锁 支持公平锁、tryLock、interrupt
性能 JVM 优化后相当 稍高(用户态)
场景 简单同步 复杂场景 注意:synchronized 在 Java 6+ 后进行了大量优化(如锁升级),性能已接近 ReentrantLock。 2. 从字节码层面剖析 synchronized synchronized 在编译后转换为 monitorenter 和 monitorexit 指令(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?(一个正常退出,一个异常退出)。
- Mark Word(8 字节):存储 hashCode、GC 分代年龄、锁状态等。
- Class Pointer(4 字节):指向类元数据。
- Array Length(可选 4 字节):数组对象才有。
- thread_id:持有偏向锁的线程 ID。
- ptr_to_lock_record:指向线程栈中 Lock Record 的指针(轻量级锁)。
- ptr_to_monitor:指向 Monitor 对象的指针(重量级锁,基于 ObjectMonitor 实现)。
- biased_lock:是否启用偏向锁(默认 1,JVM 参数 -XX:BiasedLockingStartupDelay=0 可立即启用)。
import org.openjdk.jol.info.ClassLayout; Object obj = new Object(); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); // 输出对象头布局- 面试点:对象头如何存储 hashCode?(无锁/偏向锁时直接存;升级后移到 Monitor 中)。
- 无锁(Unlocked):无线程竞争。Mark Word 存储 hashCode 等。
- 偏向锁(Biased Lock):
- 场景:单线程反复访问。
- 过程:第一次进入时,CAS 将线程 ID 写入 Mark Word。后续同线程直接检查 ID,无需 CAS。
- 开销:极低(纳秒级)。
- 升级触发:其他线程竞争 → 撤销偏向(批量偏向/撤销优化)。
- 轻量级锁(Lightweight Lock / Spin Lock):
- 场景:轻微竞争,线程交替访问。
- 过程:竞争线程在栈中分配 Lock Record,CAS 将 Mark Word 指向它。自旋等待(默认 10 次,-XX:PreBlockSpin)。
- 开销:低(自旋消耗 CPU,但避免上下文切换)。
- 升级触发:自旋失败或竞争加剧 → 膨胀为重量级锁。
- 重量级锁(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 基准测试性能。
- 启用/禁用偏向锁:-XX:+UseBiasedLocking(默认开启)。
- 自旋优化:-XX:AdaptiveSpinning(自适应自旋次数)。
- 避免问题:
- 锁竞争激烈时,用 ReentrantLock 或分段锁(ConcurrentHashMap)。
- 不要在循环内加锁(锁粗化)。
- hashCode() 调用会禁用偏向锁(因为覆盖 Mark Word)。
- Java 21+ 影响:虚拟线程(Loom)下,synchronized 兼容,但重量级锁可能 pinning(固定载体线程),影响性能。推荐用 ReentrantLock 的虚拟线程模式。
- 面试高频:
- synchronized 是公平锁吗?(非公平,但可通过队列实现近似公平)。
- 为什么升级不可逆?(性能考虑,避免频繁切换)。
- 与 volatile 区别?(volatile 仅可见性,无互斥)。