synchronized全链路解析:从字节码到JVM内核的锁实现与升级策略

深入解析synchronized的底层实现:从字节码到 JVM 内核

在 Java 的多线程编程领域,synchronized是实现线程同步的重要手段。它通过对代码块或方法进行修饰,确保在同一时刻,只有一个线程能够执行被修饰的部分,从而有效解决多线程环境下对共享资源的竞争问题。synchronized的使用方式多样,既可以用于修饰实例方法(此时锁的对象为当前实例),也能够修饰静态方法(此时锁的对象为类),还可以应用于特定对象的代码块。我们从synchronized的使用场景出发,逐步解析这个经典同步工具的底层实现原理。

synchronized的使用场景主要有以下两种:

特性 同步代码块 同步方法
命名依据 修饰 synchronized (obj) { ... } 代码块 修饰 synchronized 方法声明
同步范围 仅限于 {} 内部的代码* (细粒度) 覆盖整个方法体 (粗粒度)
锁对象 显式指定 (obj) 隐式绑定 (thisClass 对象)
底层指令 显式 monitorenter / monitorexit (在块内) 隐式 ACC_SYNCHRONIZED 标志 (方法调用/返回时处理)
核心语义 同步一段特定的代码区域,需指定锁。 同步整个方法的执行,锁自动关联。

同步代码块

我们先新建一个demo类:

java 复制代码
public class Demo {
    public void func1(){
        synchronized (this){
            System.out.println("function1");
        }
    }
}

这段代码使用了 synchronized 块来对当前对象加锁。我们来看看它在 JVM 字节码中是如何实现的。

首先命令行执行命令,编译 Java 源代码文件 Demo.java,生成字节码文件 Demo.class

bash 复制代码
javac Demo.java

执行命令后会生成一个Demo.class 的文件,然后我们反编译字节码文件 Demo.class,并将详细分析结果输出到 output.txt 文件中。

bash 复制代码
 javap -p -v -c Demo.class > output.txt

参数说明

  • -p(或 --private):显示所有类和成员(包括私有成员)。
  • -v(或--verbose):输出详细信息,包括:
    • 类的基本信息(版本、访问标志、常量池等)。
    • 字段和方法的详细描述。
    • 字节码指令。
  • -c(或 --disassemble):反汇编方法体,显示字节码指令(如 invokevirtualastore_1 等)。
  • Demo.class:要分析的字节码文件。
  • > output.txt:将结果重定向到 output.txt 文件(避免在终端中刷屏,不好查看输出的反汇编结果)。

字节码分析

下面是 func1()方法的完整字节码部分,已提取并且简化:

复制代码
public void func1();
   descriptor: ()V
   flags: (0x0001) ACC_PUBLIC
   Code:
     stack=2, locals=3, args_size=1
        0: aload_0							//加载this引用
        1: dup								//赋值this引用,栈顶为两个this
        2: astore_1							//将复制的this引用存入slot 1用于后续锁操作
        3: monitorenter                       // 加锁指令,开始执行同步块
        4: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream 获取System.out对象
        7: ldc           #13                 // String function1 加载字符串
        9: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 调用println方法
       12: aload_1						//加载slot1的this引用
       13: monitorexit                       //解锁
       14: goto          22					//跳转 return 返回
       17: astore_2							//异常时存储Throwable对象到slot2
       18: aload_1							//加载同步对象
       19: monitorexit                         // 异常情况下的解锁
       20: aload_2							//加载异常对象
       21: athrow							//重新抛出异常
       22: return
     Exception table:
        from    to  target type
            4    14    17   any
           17    20    17   any
1. 操作数栈
复制代码
stack=2, locals=3, args_size=1
  • stack=2:操作数栈最大深度为 2(执行dup时需要 2 层空间)。
  • locals=3:局部变量表大小为 3,(单位:slot)其中:
    • slot 0:存储this(方法隐含参数)
    • slot 1:存储this的副本(用于monitorentermonitorexit
    • slot 2:异常时存储Throwable对象(astore_2
  • args_size=1意味着 JVM 调用该方法时,会将this引用作为参数压入操作数栈。由于func1()是实例方法(非静态),JVM 会自动将调用该方法的对象(this)作为第一个参数传递。因此,args_size=1实际上对应this引用,而不是显式编写的参数。
2.局部变量表
text 复制代码
0: aload_0         // 将 this 引用压入操作数栈
1: dup             // 复制一份 this(因为后面要存储和使用)
2: astore_1        // 将 复制的this 存储到局部变量表 slot 1
  • aload_0:表示加载第 0 号局部变量(即 this)。
  • dup:复制栈顶值(this),用于后续操作。
  • astore_1:将复制的 this 存入局部变量表索引为 1 的位置,便于后续访问同步对象。

3. 进入同步块(加锁)
text 复制代码
3: monitorenter
  • monitorenter 是进入同步块的核心字节码指令,其底层逻辑如下:
    • 若对象的监视器(monitor)计数器为 0,说明当前无线程持有锁,当前线程获取锁并将计数器加 1;
    • 若监视器已被其他线程持有,当前线程进入阻塞状态,进入监视器的入口集(Entry Set) 等待释放;
    • 若监视器已被当前线程持有(可重入特性),则计数器递增,允许线程重复进入同步块。

每个 Java 对象都内置一个监视器(monitor),其状态由对象头中的 Mark Word 维护,这是 synchronized 实现同步的底层基础:

  • Mark Word(对象头核心组件) :64 位 JVM 中占 8 字节,随锁状态动态切换存储内容:
    • 无锁状态:存储对象哈希码(HashCode)、分代年龄;
    • 偏向锁:存储偏向线程 ID、时间戳(用于优化单线程重复获取锁的场景);
    • 轻量级锁:存储线程栈中锁记录(Lock Record)的指针(基于 CAS 实现快速加锁);
    • 重量级锁:存储指向 Monitor 对象的指针(锁竞争激烈时升级为此状态)。
  • Monitor 的底层实现 :HotSpot 中由ObjectMonitor结构体实现,关键字段包含:
    • _owner:指向当前持有锁的线程;
    • _EntryList:等待获取锁的线程队列(状态为 BLOCKED);
    • _WaitSet:调用 wait() 后释放锁并等待唤醒的线程队列(状态为 WAITING/TIMED_WAITING);
    • 依赖操作系统互斥量(Mutex)实现线程阻塞,存在用户态与内核态切换开销。

Monitor 主要包含以下几个部分:

  1. Entry Set(入口集):所有竞争锁失败的线程在此阻塞,等待锁释放;
  2. Owner(锁拥有者):当前持有锁的线程,执行同步块时独占监视器;
  3. Wait Set(等待集):线程调用 wait() 后释放锁并进入此集合,需通过 notify()/notifyAll() 唤醒后重新竞争锁。

同步块执行全流程:

  1. 线程尝试进入synchronized块,检查对象头 Mark Word 的锁状态:
    • 若为无锁 / 偏向锁(且偏向当前线程),直接获取锁;
    • 若为轻量级锁,通过 CAS 尝试将 Mark Word 指向自身栈帧的锁记录:
      • CAS 成功,获取轻量级锁,继续执行;
      • CAS 失败, 锁膨胀为重量级锁,线程进入 Monitor 的 Entry Set 阻塞。
  2. 持有锁的线程执行同步块,执行完毕后通过monitorexit释放锁:
    • 若为轻量级锁,CAS 还原 Mark Word 为无锁状态;
    • 若为重量级锁,将 Monitor 的 _owner 置为 null,唤醒 Entry Set 中的一个线程重新竞争锁。

4. 执行同步代码
text 复制代码
4: getstatic     #7   // 获取 System.out
7: ldc           #13  // 加载字符串 "synchronized"
9: invokevirtual #15  // 调用 println 方法
  • 这些指令执行的是在 synchronized 块内部写的打印语句。
  • 此时已经获得了锁,所以只有当前线程可以执行这部分代码。

5. 退出同步块(正常路径解锁)
text 复制代码
12: aload_1
13: monitorexit
14: goto          22
  • aload_1:重新加载之前保存的 this
  • monitorexit:释放锁,将 monitor 的计数器减 1;
  • goto 22:跳转到方法末尾 return

6. 退出同步块(异常路径解锁)
text 复制代码
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
  • 如果在同步块中抛出异常,JVM 也会确保调用 monitorexit 来释放锁;
  • astore_2:捕获异常对象;
  • aload_1:加载同步对象;
  • monitorexit:释放锁;
  • athrow:重新抛出异常,保证程序行为不变。

7.异常表
me 复制代码
Exception table:
from    to  target type
  4     14    17    any
 17     20    17    any

这个异常表的作用是告诉 JVM:如果在 4~1417~20 区间内发生任何异常(any),都要跳转到偏移地址 17 处处理异常,并确保释放锁。

这里有两层保护逻辑:

  1. 同步块执行区间(4~14)内任何异常,跳转到 17 行,先释放锁(19 行monitorexit)再抛异常(21 行athrow)。
  2. 异常处理区间(17~20)内若再次发生异常(如释放锁时出错),仍跳转到 17 行,确保锁一定被释放。

这是 synchronized 的重要特性之一:无论是否发生异常,锁都会被释放,避免死锁,而Lock需要在finally中手动调用unlock(),避免死锁风险。

8.底层机制示意图

同步方法

我们在之前的类中新增一个同步方法

java 复制代码
public synchronized void func2() {
    System.out.println("function2");
}

然后根据之前的步骤,生成字节码文件,已提取并且简化:

复制代码
public synchronized void func2();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
  stack=2, locals=1, args_size=1
     0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
     3: ldc           #13                 // String function2
     5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     8: return
  LineNumberTable:
    line 11: 0
    line 12: 8

从字节码文件中可以发现,用synchronized修饰方法时,不会显示使用指令,而是使用 ACC_SYNCHRONIZED 方法标志

1. 方法标志
复制代码
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
  • ACC_PUBLIC:公共访问权限
  • ACC_SYNCHRONIZED关键同步标志 ,表示该方法为同步方法
    • 此标志告知 JVM:该方法执行时需要隐式同步机制
    • 不需要显式的 monitorenter/monitorexit 指令
    • 锁对象由方法类型决定:
      • 实例方法 → this(当前对象实例)
      • 静态方法 → Class 对象(方法所属类)
2. 操作数栈与局部变量表
复制代码
stack=2, locals=1, args_size=1
  • stack=2:操作数栈最大深度为 2
    • getstatic 压入 PrintStream 对象(1层)
    • ldc 压入字符串常量(2层)
    • invokevirtual 消耗这两个操作数
  • locals=1:局部变量表仅需 1 个 slot
    • slot 0:存储隐式 this 引用(实例方法)
    • 注意 :无额外 slot 存储锁对象(对比同步代码块的 locals=3
  • args_size=1:隐含参数 this(实例方法特征)
3. 方法体字节码
复制代码
0: getstatic     #7   // 获取 System.out
3: ldc           #13  // 加载字符串 "function2"
5: invokevirtual #15  // 调用 println 方法
8: return
  • 关键特征无锁操作指令
    • 没有 monitorenter
    • 没有 monitorexit
    • 没有异常处理表(对比同步代码块的复杂异常表)
4. 同步机制实现原理
  • 加锁时机 :当 JVM 执行方法调用指令(如 invokevirtual)时
    1. 检查方法的 access_flags 是否包含 ACC_SYNCHRONIZED
    2. 若是同步方法,隐式获取锁
      • 实例方法 → 获取 this 的锁
      • 静态方法 → 获取 Class 对象的锁
  • 解锁时机 :方法执行结束时(无论正常返回或异常抛出)
    1. 正常返回(return)时释放锁
    2. 异常抛出时,JVM 在异常传播前自动释放锁
  • 异常安全 :由 JVM 在方法退出路径统一处理锁释放
    • 不需要显式的异常表(对比同步代码块的双重 monitorexit
    • 确保锁在任何退出路径都被释放
5.底层机制示意图

锁状态升级:从偏向锁到重量级锁

synchronized在 JDK 1.6 后引入锁升级机制,避免直接使用重量级锁带来的性能开销,锁的升级路径为:无锁-> 偏向锁 -> 轻量级锁-> 重量级锁。我们接下来通过一个案例来查看锁升级的过程。

我们之前提到过,每个 Java 对象都内置一个监视器(monitor),其状态由对象头中的 Mark Word维护,这是 synchronized 实现同步的底层基础。锁的升级实际上就是 Mark Word 存储的内容的变化。这个案例利用 JOL (Java Object Layout) 库提供的一个方法ClassLayout.parseInstance(biasedObj).toPrintable() ,这个方法的作用是查看 Java 对象在内存中的布局信息,特别是对象头 (Object Header) 的内容,对象头主要由 Mark Word Klass Pointer 构成,利用方法输出后的内容如下所示:

复制代码
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

我们只需要关注 Mark Word的内容, 0x0000000000000001 (non-biasable; age: 0) 表示当前为无锁且不可偏向的状态。

JVM 默认会延迟一段时间(默认 4 秒)再启用偏向锁。在这段 "延迟期" 内,偏向锁处于未激活状态,此时新建的对象是 "无锁且不可偏向" 的(对象头标志位为001,即无锁状态)。当等待时间超过这个延迟阈值后,JVM 认为系统已稳定,线程竞争减少,此时启用偏向锁。新创建的对象会默认处于 "可偏向状态"(对象头标志位为101),等待第一个线程获取它,之后便会 "偏向" 该线程(记录线程 ID 到对象头),实现无竞争时的高效同步。所以我们创建第一个无锁对象后,等待5s再进行创建第二个对象。

java 复制代码
public static void main(String[] args) throws InterruptedException {
        // 阶段1:JVM启动初期(未启用偏向锁)------无锁状态
        Object noLockObj = new Object();
        System.out.println("===== 1. 无锁状态(JVM未启用偏向锁) =====");
        System.out.println("说明:JVM启动后默认延迟4秒启用偏向锁,此时新对象为无锁且不可偏向");
        System.out.println("对象头标志位:最后3位001(无锁),状态描述为non-biasable(不可偏向)");
        System.out.println(ClassLayout.parseInstance(noLockObj).toPrintable());

        // 等待5秒,确保JVM启用偏向锁(超过默认4秒延迟)
        System.out.println("\n===== 等待JVM启用偏向锁(5秒后) =====");
        Thread.sleep(5000);

        // 阶段2:偏向锁启用后------可偏向状态(未锁定)
        Object biasedObj = new Object(); // 偏向锁启用后创建的新对象
        System.out.println("\n===== 2. 偏向锁(可偏向,未锁定) =====");
        System.out.println("说明:JVM启用偏向锁后,新对象默认处于可偏向状态");
        System.out.println("对象头标志位:最后3位101(偏向锁),状态描述为biasable(可偏向)");
        System.out.println(ClassLayout.parseInstance(biasedObj).toPrintable());

        // 阶段3:主线程持有偏向锁------已偏向状态
        System.out.println("\n===== 3. 偏向锁(已偏向主线程) =====");
        System.out.println("说明:单线程获取锁时,对象头记录线程ID,保持偏向锁状态");
        synchronized (biasedObj) {
            System.out.println(ClassLayout.parseInstance(biasedObj).toPrintable());
        }

        // 阶段4:其他线程竞争------偏向锁撤销(转为轻量级锁)
        Thread t = new Thread(() -> {
            System.out.println("\n===== 4. 偏向锁撤销(其他线程竞争) =====");
            System.out.println("说明:多线程竞争时,偏向锁撤销,升级为轻量级锁");
            synchronized (biasedObj) {
                System.out.println(ClassLayout.parseInstance(biasedObj).toPrintable());
            }
        }, "竞争线程t1");
        t.start();
        t.join();

        // 阶段5:轻量级锁升级为重量级锁
        // 创建两个线程交替竞争锁,导致轻量级锁升级为重量级锁
        Thread t2 = new Thread(() -> {
            synchronized (biasedObj) {
                try {
                    System.out.println("\n===== 5. 轻量级锁(线程t2持有) =====");
                    System.out.println("说明:线程t2获取对象锁,此时为轻量级锁状态");
                    System.out.println(ClassLayout.parseInstance(biasedObj).toPrintable());
                    // 保持锁一段时间,确保另一个线程会竞争
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "竞争线程t2");

        Thread t3 = new Thread(() -> {
            // 等待t2先获取锁
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (biasedObj) {
                System.out.println("\n===== 6. 重量级锁(线程t3持有) =====");
                System.out.println("说明:多线程交替竞争导致轻量级锁升级为重量级锁");
                System.out.println("对象头标志位:最后2位10(重量级锁)");
                System.out.println(ClassLayout.parseInstance(biasedObj).toPrintable());
            }
        }, "竞争线程t3");

        t2.start();
        t3.start();
        t2.join();
        t3.join();

        System.out.println("\n总结:无锁(偏向锁启用前)→ 偏向锁(启用后)→ 轻量级锁(遇竞争)→ 重量级锁(多线程交替竞争)")
    }

输出:

复制代码
===== 1. 无锁状态(JVM未启用偏向锁) =====
说明:JVM启动后默认延迟4秒启用偏向锁,此时新对象为无锁且不可偏向
对象头标志位:最后3位001(无锁),状态描述为non-biasable(不可偏向)
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


===== 等待JVM启用偏向锁(5秒后) =====

===== 2. 偏向锁(可偏向,未锁定) =====
说明:JVM启用偏向锁后,新对象默认处于可偏向状态
对象头标志位:最后3位101(偏向锁),状态描述为biasable(可偏向)
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


===== 3. 偏向锁(已偏向主线程) =====
说明:单线程获取锁时,对象头记录线程ID,保持偏向锁状态
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000001b7ff550805 (biased: 0x000000006dffd542; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


===== 4. 偏向锁撤销(其他线程竞争) =====
说明:多线程竞争时,偏向锁撤销,升级为轻量级锁
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00000053672ff130 (thin lock: 0x00000053672ff130)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


===== 5. 轻量级锁(线程t2持有) =====
说明:线程t2获取对象锁,此时为轻量级锁状态
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00000053672ff438 (thin lock: 0x00000053672ff438)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


===== 6. 重量级锁(线程t3持有) =====
说明:多线程交替竞争导致轻量级锁升级为重量级锁
对象头标志位:最后2位10(重量级锁)
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000001b79a179eaa (fat lock: 0x000001b79a179eaa)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


总结:无锁(偏向锁启用前)→ 偏向锁(启用后)→ 轻量级锁(遇竞争)→ 重量级锁(多线程交替竞争)

Process finished with exit code 0

注意:

​ 在 Java 15 版本中,偏向锁被正式禁用,默认情况下,不会再使用偏向锁,当我们使用Java 17 执行案例代码时,输出中不再包含偏向锁,可以看到从无锁状态直接升级为轻量级锁

复制代码
===== 2. 偏向锁(可偏向,未锁定) =====
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x00000d58
 12   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

===== 3. 偏向锁(已偏向主线程) =====
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000007b957ff180 (thin lock: 0x0000007b957ff180)
  8   4        (object header: class)    0x00000d58
 12   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

锁升级示意图:

相关推荐
Hellyc8 分钟前
用户查询优惠券之缓存击穿
java·redis·缓存
今天又在摸鱼35 分钟前
Maven
java·maven
老马啸西风38 分钟前
maven 发布到中央仓库常用脚本-02
java·maven
代码的余温38 分钟前
MyBatis集成Logback日志全攻略
java·tomcat·mybatis·logback
一只叫煤球的猫2 小时前
【🤣离谱整活】我写了一篇程序员掉进 Java 异世界的短篇小说
java·后端·程序员
斐波娜娜2 小时前
Maven详解
java·开发语言·maven
Bug退退退1232 小时前
RabbitMQ 高级特性之事务
java·分布式·spring·rabbitmq
程序员秘密基地2 小时前
基于html,css,vue,vscode,idea,,java,springboot,mysql数据库,在线旅游,景点管理系统
java·spring boot·mysql·spring·web3
皮皮林5512 小时前
自从用了CheckStyle插件,代码写的越来越规范了....
java
小码氓3 小时前
Java填充Word模板
java·开发语言·spring·word