一、前言
在上一篇文章中,我们掌握了其基础用法、核心特性及适用场景,知道它能解决并发编程的原子性、可见性、有序性问题。但你是否好奇:同样是加锁,synchronized为何能实现"隐式管理"?锁的状态是如何存储的?线程之间的锁竞争是如何被JVM调度的?
本文将深入Java虚拟机(JVM)底层,从"锁的载体(对象头)""锁的触发指令(字节码)""锁的核心调度机制(monitor)"三个维度,完整拆解synchronized的实现逻辑,带你从"会用"进阶到"懂原理",真正理解隐式锁的本质。
二、对象头与Mark Word
Java中所有对象都可以作为synchronized的锁对象,这并非偶然------每个Java对象在内存中都包含一个"对象头"结构,而对象头中的"Mark Word"(标记字)正是synchronized锁状态的核心存储载体。简单来说:synchronized的锁,本质是对对象头Mark Word的状态修改与竞争。
1. Java对象的内存布局
在HotSpot虚拟机中,Java对象的内存布局分为三部分:
- **对象头(Header) :**存储对象的核心元数据,包括锁状态、线程ID、类指针等,是synchronized锁机制的核心依赖;
- **实例数据(Instance Data) :**存储对象的成员变量(包括从父类继承的变量),是对象的核心业务数据;
- **对齐填充(Padding) :**HotSpot虚拟机要求对象内存大小必须是8字节的整数倍,对齐填充仅用于补全字节数,无实际业务意义。
其中,对象头是我们关注的重点,它又分为以下部分:
- **Mark Word :**占4字节(32位虚拟机)或8字节(64位虚拟机),存储锁状态、偏向线程ID、CAS指针、对象哈希码、GC分代年龄等信息;
- **Klass Pointer(类指针) :**占4字节(32位虚拟机)或8字节(64位虚拟机,开启压缩指针后为4字节),指向对象所属类的Class对象,用于确定对象的类型。
- **Array Length(数组长度):**占4字节(32位虚拟机)或8字节(64位虚拟机),存储数组的元素个数,数组长度是只有数组对象才有,普通对象没有。
2. Mark Word的结构与锁状态关联
Mark Word的结构并非固定不变,而是会根据对象的"锁状态"动态变化------JDK1.6为synchronized引入锁优化后,锁状态分为4种:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。不同状态下,Mark Word存储的信息不同,目的是在不同并发场景下平衡性能与安全性。
以64位HotSpot虚拟机为例,Mark Word的结构如下图所示:

其中各部分的含义如下:
-
lock: 2位的锁状态标记位,该标记的值不同,整个 Mark Word表示的含义不同。biased_lock 和 lock一起,表达的锁状态含义如上图所示;
-
biased_lock: 对象是否启用偏向锁标记 ,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock 和 biased_lock共同表示对象处于什么锁状态;
-
**age:**4位的Java对象年龄。
-
identity_hashcode:31位 的对象标识hashCode;
-
**thread:**持有偏向锁的线程ID;
-
**epoch:**偏向锁的时间戳;
-
**ptr_to_lock_record:**轻量级锁状态下,指向栈中锁记录的指针;
-
**ptr_to_heavyweight_monitor:**重量级锁状态下,指向对象监视器 Monitor的指针;
关键说明:
- 锁状态由"偏向锁标志位"和"锁标志位"共同决定(例如:偏向锁标志位1+锁标志位01=偏向锁;偏向锁标志位0+锁标志位00=轻量级锁);
- 随着并发竞争加剧,锁状态会从"无锁→偏向锁→轻量级锁→重量级锁"逐步升级,且升级过程不可逆(一旦升级为重量级锁,无法回退为轻量级锁或偏向锁);
- Mark Word是synchronized锁机制的"核心数据结构",所有锁的获取与释放,本质都是对Mark Word中锁状态的修改。
三、锁的触发与释放
synchronized是"隐式锁",其核心优势在于无需手动调用"lock()""unlock()"方法------这种隐式管理的实现,依赖于JVM在编译阶段为synchronized修饰的代码插入特定的字节码指令。不同用法(修饰方法、修饰代码块)对应的字节码实现略有差异,但核心逻辑一致。
1. 修饰代码块:monitorenter与monitorexit指令
当synchronized修饰代码块时,JVM会在代码块的"进入处"插入 monitorenter 指令,在"退出处"(包括正常退出和异常退出)插入 monitorexit 指令。这两个指令是锁获取与释放的直接触发者。
代码示例与字节码分析:
java
public class SyncBlockDemo {
private final Object lock = new Object();
public void syncBlock() {
// 同步代码块
synchronized (lock) {
System.out.println("进入同步代码块");
}
}
}
使用 javap -v SyncBlockDemo.class 命令反编译,可得到 syncBlock 方法的字节码(核心部分):
java
public void syncBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #2 // Field lock:Ljava/lang/Object;
4: dup
5: monitorenter // 进入同步代码块,获取锁(核心指令)
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #4 // String 进入同步代码块
11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: aload_1
15: monitorexit // 正常退出同步代码块,释放锁(核心指令)
16: goto 24
19: astore_2
20: aload_1
21: monitorexit // 异常退出同步代码块,释放锁(核心指令)
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any
}
字节码核心逻辑解读:
-
0-4行:通过 getfield 指令获取锁对象 lock ,并通过 dup 指令复制一份锁对象引用(用于后续 monitorenter 和 monitorexit 操作);
-
5行: monitorenter 指令------线程尝试获取锁:若锁未被持有,则将锁的持有者设为当前线程,锁计数器+1;若锁已被当前线程持有,则锁计数器+1(可重入性);若锁被其他线程持有,则当前线程阻塞;
-
6-11行:执行同步代码块的核心逻辑(打印语句);
-
15行: monitorexit 指令(正常退出)------释放锁,锁计数器-1,当计数器减为0时,锁被完全释放,唤醒等待锁的线程;
-
19-23行:异常退出分支------JVM为同步代码块自动生成异常处理逻辑,确保即使发生异常,也能通过 monitorexit 释放锁,避免锁泄漏。
关键结论: monitorenter 和 monitorexit 是synchronized修饰代码块的"锁开关",JVM通过这两个指令实现锁的获取与释放,且异常场景的锁释放由JVM自动保障。
2. 修饰方法:ACC_SYNCHRONIZED标志位
当synchronized修饰实例方法或静态方法时,JVM不会插入 monitorenter 和 monitorexit 指令,而是通过在方法的"访问标志(accessflags)"中添加 ACCSYNCHRONIZED 标志位来实现锁机制。
代码示例与字节码分析:
java
public class SyncMethodDemo {
// 同步实例方法
public synchronized void syncInstanceMethod() {
System.out.println("进入同步实例方法");
}
// 同步静态方法
public static synchronized void syncStaticMethod() {
System.out.println("进入同步静态方法");
}
}
反编译后,方法的字节码核心部分如下:
java
// 同步实例方法
public synchronized void syncInstanceMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 新增ACC_SYNCHRONIZED标志位
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String 进入同步实例方法
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
// 同步静态方法
public static synchronized void syncStaticMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED // 新增ACC_SYNCHRONIZED标志位
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String 进入同步静态方法
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
字节码核心逻辑解读:
- 同步方法的字节码中,没有 monitorenter 和 monitorexit 指令,而是通过 ACC_SYNCHRONIZED 标志位标识"这是一个同步方法";
- 当线程调用带有 ACC_SYNCHRONIZED 标志的方法时,JVM会自动尝试获取锁:同步实例方法:锁对象为当前实例(this),获取锁的逻辑与 monitorenter 一致;同步静态方法:锁对象为当前类的Class对象,获取锁的逻辑与 monitorenter 一致;
- 方法执行完成(正常return或异常抛出)时,JVM会自动释放锁,无需手动处理。
关键结论:synchronized修饰方法的锁机制,本质是JVM对 ACC_SYNCHRONIZED 标志位的解析------将锁的获取与释放逻辑嵌入到方法的调用与返回流程中,实现隐式管理。
四、核心锁调度机制
无论是 monitorenter 指令,还是 ACC_SYNCHRONIZED 标志位,最终都依赖于"monitor"对象实现锁的调度。monitor是操作系统层面的"互斥量(mutex)"的封装,是synchronized重量级锁的核心载体,负责管理线程的锁竞争与等待。
1. monitor的本质与结构
monitor的本质是一个"对象",在HotSpot虚拟机中,它由C++语言实现(对应 ObjectMonitor 类)。每个Java对象在创建时,都会关联一个monitor对象(可理解为"对象→monitor"的一对一映射),当线程尝试获取锁时,实际上是在竞争monitor的"所有权"。
ObjectMonitor 的核心结构如下(简化版):
java
class ObjectMonitor {
// 持有当前monitor的线程(锁的所有者)
Thread* owner;
// 等待锁的线程队列(阻塞状态)
Queue* EntryList;
// 调用wait()后等待唤醒的线程队列
Queue* WaitSet;
// 锁计数器(实现可重入性)
int count;
// 递归锁计数器(记录重入次数)
int recursions;
};
2. 基于monitor的锁竞争流程
当多个线程竞争同一把锁时,JVM通过monitor的 owner 、 EntryList 、 WaitSet 三个核心组件实现调度,完整流程如下:
-
**初始状态 :**monitor的 owner 为null, count 为0, EntryList 和 WaitSet 为空;
-
**线程1获取锁 :**线程1执行 monitorenter 指令(或调用同步方法),发现monitor的 owner 为null,直接成为 owner , count 置为1,进入临界区执行代码;
-
**线程2竞争锁 :**线程2尝试获取锁时,发现monitor的 owner 已为线程1,无法获取,被JVM放入 EntryList 队列,进入阻塞状态(BLOCKED);
-
**线程1释放锁 :**线程1执行完临界区代码,执行 monitorexit 指令(或方法返回), count 减为0,释放monitor的 owner (置为null),并唤醒 EntryList 中的线程(如线程2);
-
**线程2再次竞争 :**被唤醒的线程2重新尝试获取锁,若此时monitor的 owner 为null,则成为新的 owner , count 置为1,进入临界区;若仍有其他线程竞争,则再次进入 EntryList 阻塞;
-
**线程调用wait()方法 :**若线程1在持有锁期间调用了 wait() 方法,则会释放monitor的 owner , count 置为0,自身进入 WaitSet 队列,进入等待状态(WAITING);
-
**线程被notify()唤醒 :**当其他线程调用同一锁对象的 notify() 或 notifyAll() 方法时,JVM会将 WaitSet 中的线程(如线程1)转移到 EntryList 队列,等待再次竞争锁。
3. 重量级锁的性能瓶颈根源
在JDK1.6之前,synchronized直接使用上述monitor机制(即重量级锁),导致性能较差。其核心瓶颈在于:
- **线程状态切换成本高 :**线程从 EntryList 的阻塞状态(BLOCKED)被唤醒后,需要从"内核态"切换到"用户态",而状态切换涉及操作系统内核的调度,耗时较长;
- **锁竞争的排他性开销 :**即使只有两个线程交替竞争锁,也需要频繁进行"阻塞→唤醒"的状态切换,无法充分利用CPU资源。
这也是JDK1.6引入偏向锁、轻量级锁优化的核心原因------在低并发场景下,避免使用重量级锁的内核态调度,提升锁竞争效率。
五、可重入性的底层实现
在上一文中我们提到,synchronized具备可重入性------同一线程可以多次获取同一把锁,不会因已持有锁而阻塞。这一特性的底层实现,依赖于monitor的 count (锁计数器)和 recursions (递归锁计数器)。
具体实现逻辑:
-
线程第一次获取锁时,monitor的 owner 设为当前线程, count 置为1, recursions 置为0;
-
线程再次获取同一把锁时(如同步方法调用同步方法),JVM检测到当前线程已是monitor的 owner ,则直接将 count 加1, recursions 加1(记录重入次数);
-
线程每次释放锁时, count 减1;
-
当 count 减为0时,说明线程已完全释放锁, owner 置为null,其他线程可竞争。
**示例验证:**
java
public class ReentrantDemo {
public synchronized void methodA() {
System.out.println("进入methodA");
methodB(); // 同一线程调用同步方法,重入锁
}
public synchronized void methodB() {
System.out.println("进入methodB");
}
public static void main(String[] args) {
new ReentrantDemo().methodA();
}
}
执行流程:
-
线程调用 methodA() ,获取锁,monitor的 count=1 ;
-
线程在 methodA() 中调用 methodB() ,再次获取同一把锁,monitor的 count=2 ;
-
methodB() 执行完成,释放锁, count=1 ;
-
methodA() 执行完成,释放锁, count=0 ,锁完全释放。
关键结论:可重入性的核心是"锁计数器"------通过计数记录线程的重入次数,确保线程只有在完全释放所有重入锁后,才会让出锁的所有权。
六、内存可见性的底层保障
在上一文中我们提到,synchronized能保证内存可见性------一个线程对共享变量的修改,会被后续获取同一把锁的线程及时感知。这一特性的底层实现,依赖于JVM在锁的获取与释放过程中插入的"内存屏障"。
1. 内存屏障的核心作用
内存屏障是CPU层面的指令,用于禁止指令重排序,并强制刷新工作内存与主内存的数据。JVM通过插入内存屏障,确保:
- 线程对共享变量的修改,必须同步到主内存;
- 线程读取共享变量时,必须从主内存加载最新数据。
2. synchronized的内存屏障插入规则
JVM为synchronized的锁获取与释放过程,制定了严格的内存屏障插入规则:
- **锁获取时 :**在 monitorenter 指令(或同步方法调用)后,插入"LoadLoad屏障"和"LoadStore屏障":
-
**LoadLoad屏障:**禁止后续的读操作与当前读操作重排序;
-
**LoadStore屏障:**禁止后续的写操作与当前读操作重排序;
-
**核心效果:**强制线程从主内存加载共享变量的最新数据,避免读取旧值。
- **锁释放时 :**在 monitorexit 指令(或同步方法返回)前,插入"StoreStore屏障"和"StoreLoad屏障":
-
**StoreStore屏障:**禁止当前写操作与后续写操作重排序;
-
**StoreLoad屏障:**禁止当前写操作与后续读操作重排序,并强制将工作内存中的数据同步到主内存;
-
**核心效果:**确保线程对共享变量的修改已同步到主内存,让其他线程可见。
3. 与volatile的可见性机制对比
synchronized与volatile都能保证内存可见性,但实现逻辑不同:
| 特性 | synchronized | volatile |
| 可见性实现方式 | 通过锁获取/释放时插入的内存屏障,间接保证可见性 | 直接在写操作后插入StoreLoad屏障,读操作前插入LoadLoad屏障,直接保证可见性 |
| 额外保障 | 同时保证原子性、有序性 | 仅保证可见性、有序性,不保证原子性 |
| 适用场景 | 临界区代码(多操作组合) | 单个共享变量的读/写操作 |
|---|
七、总结
本文从JVM底层视角,完整拆解了synchronized的实现逻辑,核心可总结为"三个核心载体+一个调度机制":
-
**锁的存储载体 :**对象头的Mark Word,通过动态修改锁状态(无锁→偏向锁→轻量级锁→重量级锁)适配不同并发场景;
-
**锁的触发指令 :**修饰代码块时通过 monitorenter / monitorexit 指令,修饰方法时通过 ACC_SYNCHRONIZED 标志位,实现隐式锁管理;
-
**锁的调度机制 :**基于monitor对象( ObjectMonitor )的 owner 、 EntryList 、 WaitSet 组件,实现线程的锁竞争与等待唤醒;
-
**可见性与可重入性保障 :**通过内存屏障保证可见性,通过monitor的锁计数器实现可重入性。
理解这些底层原理,能帮你更深刻地理解synchronized的性能特性------为何JDK1.6要引入锁优化?为何偏向锁适合单线程场景?为何高并发下synchronized性能会下降?这些问题,我们将在第3篇"synchronized锁优化深度解析"中详细解答。
下一篇文章,我们将聚焦JDK1.6的锁优化机制,深入剖析偏向锁、轻量级锁、重量级锁的实现细节与升级流程,带你理解synchronized性能提升的核心逻辑,敬请关注!