深入解析synchronized
的底层实现:从字节码到 JVM 内核
在 Java 的多线程编程领域,synchronized
是实现线程同步的重要手段。它通过对代码块或方法进行修饰,确保在同一时刻,只有一个线程能够执行被修饰的部分,从而有效解决多线程环境下对共享资源的竞争问题。synchronized
的使用方式多样,既可以用于修饰实例方法(此时锁的对象为当前实例),也能够修饰静态方法(此时锁的对象为类),还可以应用于特定对象的代码块。我们从synchronized
的使用场景出发,逐步解析这个经典同步工具的底层实现原理。
synchronized
的使用场景主要有以下两种:
特性 | 同步代码块 | 同步方法 |
---|---|---|
命名依据 | 修饰 synchronized (obj) { ... } 代码块 |
修饰 synchronized 方法声明 |
同步范围 | 仅限于 {} 内部的代码* (细粒度) |
覆盖整个方法体 (粗粒度) |
锁对象 | 显式指定 (obj ) |
隐式绑定 (this 或 Class 对象) |
底层指令 | 显式 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
):反汇编方法体,显示字节码指令(如invokevirtual
、astore_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
的副本(用于monitorenter
和monitorexit
)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 主要包含以下几个部分:
- Entry Set(入口集):所有竞争锁失败的线程在此阻塞,等待锁释放;
- Owner(锁拥有者):当前持有锁的线程,执行同步块时独占监视器;
- Wait Set(等待集):线程调用
wait()
后释放锁并进入此集合,需通过notify()
/notifyAll()
唤醒后重新竞争锁。
同步块执行全流程:
- 线程尝试进入
synchronized
块,检查对象头 Mark Word 的锁状态:- 若为无锁 / 偏向锁(且偏向当前线程),直接获取锁;
- 若为轻量级锁,通过 CAS 尝试将 Mark Word 指向自身栈帧的锁记录:
- CAS 成功,获取轻量级锁,继续执行;
- CAS 失败, 锁膨胀为重量级锁,线程进入 Monitor 的 Entry Set 阻塞。
- 持有锁的线程执行同步块,执行完毕后通过
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~14
或 17~20
区间内发生任何异常(any
),都要跳转到偏移地址 17
处处理异常,并确保释放锁。
这里有两层保护逻辑:
- 同步块执行区间(4~14)内任何异常,跳转到 17 行,先释放锁(19 行
monitorexit
)再抛异常(21 行athrow
)。 - 异常处理区间(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
:操作数栈最大深度为 2getstatic
压入PrintStream
对象(1层)ldc
压入字符串常量(2层)invokevirtual
消耗这两个操作数
locals=1
:局部变量表仅需 1 个 slotslot 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
)时- 检查方法的
access_flags
是否包含ACC_SYNCHRONIZED
- 若是同步方法,隐式获取锁 :
- 实例方法 → 获取
this
的锁 - 静态方法 → 获取
Class
对象的锁
- 实例方法 → 获取
- 检查方法的
- 解锁时机 :方法执行结束时(无论正常返回或异常抛出)
- 正常返回(
return
)时释放锁 - 异常抛出时,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
锁升级示意图: