深入剖析 Java synchronized:底层实现原理与核心特性全解析
一、基础铺垫:synchronized 与 Java 对象的绑定关系
要理解 synchronized,首先要明确一个核心前提:Java 中每一个对象都隐式关联了一把 “对象锁”,而 synchronized 的本质就是对 “对象锁” 的操作。无论是 synchronized (obj) 修饰代码块,还是 synchronized 修饰实例方法(锁为 this)、静态方法(锁为类对象 Class),最终都是通过操作 “对象关联的锁机制” 实现同步。
而支撑这一机制的核心,是 Java 对象的内存结构—— 尤其是对象头中的 MarkWord(标记字段),它是锁状态的 “记录仪”,也是 synchronized 底层逻辑的关键载体。
二、对象内存结构:锁机制的 “物理基础”
每个 Java 对象在堆内存中存储时,都会分为 3 个部分,其中对象头是实现锁机制的核心:
| 内存区域 | 作用说明 |
|---|---|
| 对象头(Header) | 存储对象元数据(锁状态、哈希码、GC 分代年龄等),是 synchronized 的核心依赖。 |
| 实例数据(Instance Data) | 存储对象的成员变量值(如类的字段),是线程安全问题的 “数据载体”。 |
| 对齐填充(Padding) | JVM 要求对象内存大小为 8 字节的整数倍,不足时用此部分补齐,保证内存对齐。 |
重点:对象头的构成
对象头由两部分组成,其中 MarkWord 是锁机制的关键:
- 类型指针(Klass Pointer):指向对象对应的类元数据(如
User.class),用于确定对象的类型,JVM 通过它找到对象的类信息。 - MarkWord(标记字段):存储对象的锁状态、持有锁的线程 ID、指向
monitor的指针等核心信息。其结构会随锁状态动态变化(无锁、偏向锁、轻量级锁、重量级锁),是锁升级的 “状态指示器”。
三、MarkWord:锁状态的 “动态记录仪”
MarkWord 是对象头中最核心的部分,其长度在 32 位 JVM 中为 4 字节(32 位),64 位 JVM 中为 8 字节(64 位)。它的结构会根据 “锁状态” 动态调整,以最大化利用内存空间。以下以 32 位 JVM 为例,展示不同锁状态下 MarkWord 的存储内容:
| 锁状态 | MarkWord 存储结构(32 位) | 核心作用 |
|---|---|---|
| 无锁状态 | 哈希码(25 位) + 分代年龄(4 位) + 无锁标记(3 位) | 未被任何线程锁定,存储对象的哈希码(第一次调用 hashCode() 时计算)和 GC 分代年龄(用于 GC 回收判断)。 |
| 偏向锁状态 | 线程 ID(23 位) + Epoch(2 位) + 分代年龄(4 位) + 偏向锁标记(3 位) | 记录 “偏向的线程 ID”,Epoch 用于批量撤销偏向锁(JVM 优化手段),避免频繁修改线程 ID。 |
| 轻量级锁状态 | 锁记录指针(30 位) + 轻量级锁标记(2 位) | 指向线程栈帧中 “锁记录(Lock Record)” 的指针,通过 CAS 操作实现锁的获取与释放。 |
| 重量级锁状态 | monitor 指针(30 位) + 重量级锁标记(2 位) | 存储指向 monitor 对象的指针(ptr_to_heavyweight_monitor),关联重量级锁的核心同步机制。 |
关键结论:MarkWord 是 synchronized 锁状态的 “载体”—— 通过修改 MarkWord 的结构,JVM 实现了 “锁状态切换” 和 “锁信息存储”,为后续的锁升级和同步操作提供基础。
四、monitor:重量级锁的 “同步核心”
当 synchronized 升级为重量级锁时,其底层依赖的是 monitor(管程)机制。monitor 是一种操作系统级的同步原语,本质是一个 “对象监视器”,用于保证多线程对共享资源的互斥访问和线程协调。
1. monitor 的核心结构
每个 monitor 内部包含 3 个关键组件,共同实现线程的 “锁竞争” 和 “等待唤醒” 逻辑:
| 组件名称 | 作用说明 |
|---|---|
| Owner(持有线程) | 记录当前持有 monitor 的线程,同一时间仅能有一个线程成为 Owner(保证锁的独占性)。支持锁重入:同一线程多次获取锁时,会通过 “计数器” 记录重入次数(每次获取 +1,释放 -1,计数器为 0 时真正释放锁)。 |
| EntryList(入口集) | 存储尝试获取 monitor 但失败的线程,这些线程处于 BLOCKED 状态,等待 Owner 释放锁后重新竞争。 |
| WaitSet(等待集) | 存储调用 wait() 方法的线程:持有锁的线程调用 wait() 后,会释放 monitor 并进入 WaitSet,处于 WAITING/TIMED_WAITING 状态,需等待其他线程调用 notify()/notifyAll() 唤醒后,重新进入 EntryList 竞争锁。 |
2. monitor 与对象的关联方式
monitor 并非直接绑定对象,而是通过 MarkWord 间接关联:
- 当锁升级为重量级锁时,JVM 会为对象分配一个
monitor对象,并将MarkWord中的 “ptr_to_heavyweight_monitor” 字段设置为monitor的内存地址。 - 后续线程对 “对象锁” 的操作(获取 / 释放),本质是通过
MarkWord找到对应的monitor,再对monitor的Owner、EntryList等组件进行操作。
3. 重量级锁的性能特点
monitor 涉及 用户态与内核态的切换:线程的阻塞(进入 EntryList)和唤醒(从 WaitSet 唤醒)需要操作系统介入,而上下文切换的成本较高。因此,重量级锁仅在 “多线程高频竞争” 场景下使用,JVM 会通过 “锁升级” 机制尽量避免直接使用重量级锁。
五、锁升级:synchronized 的 “性能优化核心”
早期的 synchronized 因直接使用重量级锁,性能较差。JDK 6 后,JVM 引入 “锁升级” 机制,通过无锁→偏向锁→轻量级锁→重量级锁的动态切换,在 “低竞争” 场景下减少同步开销,平衡性能与安全性。
锁升级的核心逻辑:根据线程竞争的激烈程度,动态选择成本最低的锁机制。
1. 阶段 1:偏向锁(无实际竞争)
- 适用场景:只有一个线程重复获取锁(无多线程竞争,如单线程循环加锁)。
- 实现逻辑:
- 线程第一次获取锁时,通过 CAS 操作将
MarkWord中的 “线程 ID” 设置为当前线程 ID,并将锁标记设为 “偏向锁”。 - 后续该线程再次获取锁时,无需 CAS 操作,仅需对比
MarkWord中的线程 ID 是否为自己:- 是:直接重入锁(无任何开销)。
- 否:说明出现线程竞争,需 “撤销偏向锁”(暂停持有偏向锁的线程,检查锁状态),并升级为轻量级锁。
- 线程第一次获取锁时,通过 CAS 操作将
- 核心优势:几乎无同步开销,仅第一次加锁需 CAS,后续重入仅需对比线程 ID。
2. 阶段 2:轻量级锁(轻度竞争)
-
适用场景:多个线程交替获取锁(无同时竞争,如线程 A 释放锁后,线程 B 再获取)。
-
实现逻辑:
加锁过程:
- 线程在栈帧中创建一个 Lock Record(锁记录),存储两部分信息:
Object Reference:指向锁对象的引用。Displaced MarkWord:复制锁对象MarkWord的当前值(作为解锁时的恢复依据)。
- 通过 CAS 操作将锁对象的
MarkWord替换为 “指向当前 Lock Record 的指针”:- 成功:获取锁,将
MarkWord的锁标记设为 “轻量级锁”。 - 失败:说明有线程同时竞争锁(如线程 A 加锁时,线程 B 也在尝试加锁),轻量级锁升级为重量级锁。
- 成功:获取锁,将
解锁过程:
- 通过 CAS 操作将锁对象的
MarkWord恢复为Displaced MarkWord(即无锁状态的MarkWord):- 成功:释放锁,无竞争。
- 失败:说明锁已升级为重量级锁,需调用
monitorexit指令释放monitor,并唤醒EntryList中的线程。
- 线程在栈帧中创建一个 Lock Record(锁记录),存储两部分信息:
-
核心优势:通过 CAS 操作避免
monitor的内核态切换,适合轻度竞争场景。
3. 阶段 3:重量级锁(重度竞争)
- 适用场景:多个线程同时竞争锁(高频并发,如线程 A 未释放锁时,线程 B、C 同时尝试获取)。
- 实现逻辑:
- 当轻量级锁的 CAS 操作失败(存在同时竞争)时,JVM 会将锁升级为重量级锁:
- 将锁对象
MarkWord中的 “ptr_to_heavyweight_monitor” 设为monitor的地址。 - 未获取锁的线程进入
monitor的EntryList,处于BLOCKED状态。
- 将锁对象
- 持有锁的线程释放锁时,会调用
monitorexit指令:- 将
monitor的Owner设为 null,计数器清零。 - 唤醒
EntryList中的线程,重新竞争monitor。
- 将
- 当轻量级锁的 CAS 操作失败(存在同时竞争)时,JVM 会将锁升级为重量级锁:
- 核心劣势:涉及用户态与内核态切换,上下文切换成本高,但能保证多线程并发安全。
六、synchronized 的核心特性:原子性、可见性、有序性
synchronized 之所以能解决线程安全问题,是因为它同时保证了原子性、可见性、有序性三大核心特性。这三大特性的实现,依赖于前文提到的 “锁机制” 和 “内存屏障”。
1. 原子性保证:操作不可分割
原子性定义:一个操作或一系列操作,要么全部执行完成,要么全部不执行,中间不会被其他线程打断。
实现原理:
- 偏向锁 / 轻量级锁:通过 CAS 操作的原子性 实现。
- 偏向锁的线程 ID 设置、轻量级锁的
MarkWord替换,均通过 CPU 的 CAS 指令(不可分割)保证操作不被打断。 - 同步块内的操作因锁的独占性(同一时间仅一个线程执行),避免多线程交叉干扰。
- 偏向锁的线程 ID 设置、轻量级锁的
- 重量级锁:通过 monitor 的独占性 实现。
- 同一时间仅一个线程能成为
monitor的Owner,同步块内的操作作为 “整体” 执行,不会被其他线程打断。
- 同一时间仅一个线程能成为
示例:count++ 的原子性
count++ 本质是 “读取 count → 加 1 → 写入 count” 三步操作,不加锁时会因线程交叉执行导致错误。而 synchronized 通过锁的独占性,将这三步操作 “打包” 为原子单元,确保多线程下 count 的正确性。
2. 可见性保证:修改可被感知
可见性定义:当一个线程修改了共享变量的值后,其他线程能 “立即” 看到这个修改后的结果。
实现原理:依赖 “锁操作 + 内存屏障”
JVM 会在 synchronized 的 “获取锁” 和 “释放锁” 阶段,自动插入 内存屏障(Memory Barrier)—— 一组禁止指令重排序、强制缓存同步的 CPU 指令,确保共享变量的修改被其他线程感知。
| 锁操作 | 插入的内存屏障 | 作用说明 |
|---|---|---|
| 释放锁(退出同步块) | StoreStore 屏障 + StoreLoad 屏障 | 1. StoreStore 屏障:确保释放锁前,线程对共享变量的所有写入操作(Store)已刷新到主内存,防止后续写入重排序到释放锁前。2. StoreLoad 屏障:确保主内存已接收所有写入,防止释放锁后的读取操作(Load)重排序到释放锁前,避免其他线程读到旧值。 |
| 获取锁(进入同步块) | LoadLoad 屏障 + LoadStore 屏障 | 1. LoadLoad 屏障:确保线程从主内存加载共享变量的最新值(Load),清空本地内存的旧值,防止后续读取使用缓存旧值。2. LoadStore 屏障:确保读取操作完成后,再执行后续写入操作,避免写入使用未更新的旧值。 |
核心逻辑:释放锁时强制刷新本地内存到主内存,获取锁时强制从主内存加载最新值,确保多线程对共享变量的修改可被感知。
3. 有序性保证:避免指令重排序
有序性定义:程序执行的顺序与代码的逻辑顺序一致,避免 CPU 为优化性能而进行的 “指令重排序” 导致的执行混乱。
实现原理:
- 直接保证:通过 “锁的独占性”—— 同一时间仅一个线程执行同步块内的代码,确保同步块内的操作按逻辑顺序执行,不会与其他线程的操作交叉。
- 间接保证:通过 “内存屏障”——
StoreStore、StoreLoad、LoadLoad、LoadStore屏障禁止了指令重排序,确保同步块内的指令按代码顺序执行,且同步块外的指令不会与同步块内的指令重排序。
注意:synchronized 仅保证 “同步块内的操作相对有序”,不保证 “同步块外的操作有序”—— 这与 volatile 的 “禁止指令重排序” 范围不同,但已能满足多线程安全的需求。
七、扩展:synchronized 与 volatile 的对比
很多人会混淆 synchronized 和 volatile,两者虽都用于解决线程安全问题,但定位和能力不同,对比如下:
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | 支持(同步块内操作原子) | 不支持(仅保证单个变量的读写可见,不保证复合操作原子) |
| 可见性 | 支持(通过内存屏障强制缓存同步) | 支持(通过内存屏障禁止缓存,强制读写主内存) |
| 有序性 | 支持(通过锁独占性和内存屏障避免重排序) | 支持(通过内存屏障禁止指令重排序) |
| 锁机制 | 支持偏向锁、轻量级锁、重量级锁(动态升级) | 无锁机制(仅内存语义) |
| 适用场景 | 复合操作(如 count++、多步业务逻辑) | 单个变量的读写(如状态标记 flag) |
八、总结
synchronized 并非简单的 “锁”,而是一套从 “对象结构” 到 “锁机制”,再到 “内存语义” 的完整体系:
- 基础载体:Java 对象的
MarkWord存储锁状态,是锁升级的 “动态记录仪”。 - 核心机制:通过 “偏向锁→轻量级锁→重量级锁” 的升级流程,平衡性能与并发安全,避免不必要的内核态切换。
- 同步核心:重量级锁依赖
monitor的独占性,实现线程的互斥访问和等待唤醒。 - 特性保证:原子性靠 “锁的独占性” 或 “CAS 原子性”,可见性和有序性靠 “锁操作时插入的内存屏障”。
理解 synchronized 的底层原理,不仅能帮助你在实际开发中 “合理使用锁”(如避免过度同步导致性能问题),更能让你深入理解 JVM 对多线程的优化思路 ——用最低的成本解决最复杂的并发问题。
更多推荐


所有评论(0)