✅ 核心速览:
| 阶段 | 关键版本 | 核心特征 | 解决痛点 |
|---|---|---|---|
| 1.0 | JDK 1.0 - 1.5 | 重量级锁 | 实现基本的线程互斥,但性能差,依赖 OS。 |
| 2.0 | JDK 1.6 - 14 | 锁升级 | 引入偏向锁、轻量级锁,大幅降低无竞争下的开销。 |
| 3.0 | JDK 15 - 23 | 移除偏向锁 | 移除偏向锁,简化逻辑;但在虚拟线程下存在 Pinning 缺陷。 |
| 4.0 | JDK 24+ | 虚拟线程适配 | 重构锁底层 (JEP 491),支持虚拟线程卸载,彻底解决 Pinning。 |
synchronized锁住了什么?
1、字节码指令
-
monitorenter :当线程执行到同步代码块开头时,尝试获取锁。
-
monitorexit:当线程执行完同步块(或发生异常)时,释放锁。JVM 会自动插入两个 monitorexit 指令,确保异常发生时锁也能被释放,防止死锁。
2、对象头(Mark Word)
Java 对象在内存中都有一个"对象头",其中包含一个 Mark Word,这是最核心的部分。在 64 位 JVM 开启指针压缩的情况下,它通常占用 8 个字节。它是一个多态的数据结构,根据对象状态的不同,存储不同的信息,如下表所示。
| 锁状态 | 存储内容 (64位) | 标志位 |
|---|---|---|
| 无锁 | 对象哈希码 + GC 分代年龄 | 001 |
| 偏向锁 | 线程 ID + 偏向时间戳 + 年龄 | 101 |
| 轻量级锁 | 指向栈中 Lock Record 的指针 | 000 |
| 重量级锁 | 指向 Monitor (堆内存) 的指针 | 10 (重量级) |
3、监视器(Monitor)
这是 JVM 内部的一个 C++ 对象(ObjectMonitor)。当锁升级为"重量级锁"时,线程会进入这个 Monitor 的等待队列(EntryList)中阻塞。
在 Java 层面,我们锁的是 Object。但在 JVM 内存(堆)中,一个普通对象由三部分(对象头(Object Header)、实例数据(Instance Data) 和 对齐填充(Padding))组成,锁的秘密全藏在对象头(Object Header) 里。
锁升级(JDK 1.6 - JDK 20)
在 JDK 1.5 及之前,synchronized 是性能杀手。它直接依赖操作系统的互斥锁(Mutex),每次获取锁都要从用户态切换到内核态,开销极大。
JDK 1.6 引入了"锁升级"机制,这是 synchronized 的里程碑。JVM 发现,大多数锁其实不存在竞争,或者只被同一个线程反复获取。于是在JDK 1.6 锁的状态变成了,无锁 → 偏向锁 → 轻量级锁 → 重量级锁 四个状态动态变化的过程。
1、偏向锁
当第一个线程 A 访问同步块时,JVM 通过 CAS 指令 尝试将 Mark Word 中的线程 ID 替换为 A 的 ID。
- 成功:Mark Word 变为偏向锁状态。线程 A 以后再来,只需对比 Mark Word 里的 ID 是不是自己,完全不需要任何 CAS 或内存屏障操作。
- 撤销:当线程 B 来竞争时,JVM 必须暂停线程 A(Stop-The-World),检查 A 是否还活着。如果 A 死了,锁升级;如果 A 活着,A 必须释放锁,锁升级为轻量级锁。
因为在高并发场景下,线程频繁切换,导致偏向锁频繁撤销(需要 STW),开销反而比直接上轻量级锁更大。所以在 JDK 15 中默认禁用,JDK 17 中彻底移除。
2、轻量级锁
在有少量竞争,但还没到需要阻塞线程地步的场景下,线程通过 CAS (Compare-And-Swap) 操作尝试将对象头替换为指向自己栈帧中"锁记录"的指针。如果成功,获得锁;如果失败,说明有竞争。
整个过程在用户态完成,不用调用操作系统,速度极快。
- Lock Record (锁记录):线程进入同步块时,JVM 会在当前线程的 Java 栈帧 中分配一块空间,叫 Lock Record。它复制了当前对象头的 Mark Word(作为备份)。
- CAS 抢占 :JVM 尝试用 CAS 将对象头的 Mark Word 替换为指向栈中 Lock Record 的指针。
- 成功:获得锁
- 失败:说明有其他线程竞争。JVM 会检查对象头是否指向当前线程的栈(即重入),如果是,计数器+1;如果不是,说明是真竞争,锁升级为重量级锁
- 自旋优化:在升级为重量级锁之前,JVM 会让线程进行几次自旋(Spin),即空转 CPU 循环检查锁是否释放。(PS:如果锁持有者马上就释放了,自旋比挂起线程(内核切换)要快得多。)
3、重量级锁
当 CAS 失败且自旋无效,锁"膨胀"为重量级锁。对象头指向堆中的 Monitor 对象,竞争失败的线程被挂起(阻塞),进入操作系统的等待队列。
此时,对象头指向堆中的 ObjectMonitor 对象。其核心 C++ 结构如下:
java
class ObjectMonitor {
int _count; // 重入次数
void* _owner; // 持有锁的线程指针
ObjectWaiter* _EntryList; // 等待获取锁的队列 (Blocked)
ObjectWaiter* _WaitSet; // 调用 wait() 后的等待队列
};
竞争失败的线程被封装成 ObjectWaiter 放入 _EntryList,并调用操作系统的 park() 函数。线程状态从 Runnable 变为 Blocked,CPU 时间片被剥夺,发生 用户态 -> 内核态 的切换,性能损耗巨大。
虚拟线程带来的挑战(JDK 21 - 23)
JDK 21 引入的虚拟线程给 synchronized 带来了前所未有的危机。
- 设计冲突:虚拟线程的精髓是"遇阻即挂"------当它做 I/O 阻塞时,应该把底层的载体线程(平台线程)释放出来去干别的事。
- Pinning(钉住)问题
- 在 JDK 21-23 中,synchronized 的底层 Monitor 是绑定在载体线程上的。
- 如果一个虚拟线程持有了 synchronized 锁,然后去执行 I/O 阻塞,JVM 不敢把它卸载(挂起),因为一旦卸载,载体线程就自由了,可能会去抢别的锁,导致锁状态混乱。
- 结果,虚拟线程被死死"钉"在载体线程上,载体线程无法释放。
JDK 24 底层重构
为了解决 Pinning 问题,JDK 24 推出了 JEP 491,对 synchronized 底层数据结构进行了重构。
核心思想是:让锁不再认识载体线程,只认识虚拟线程。
JDK 24 修改了锁的底层数据结构,使其能够感知虚拟线程:
1、新轻量级锁(New Lightweight Locking)
- 旧方式:锁记录直接存在线程栈里,对象头指向栈指针。虚拟线程一挂起,栈就没了,指针就失效了。
- 新方式 :JVM 引入了一个线程局部的 Lock Stack(锁栈),具体流程如下:
1. 对象头 Mark Word 不再存指针,而是标记为"已锁定",并指向一个全局的元数据或仅仅标记状态。
2. 实际的锁记录(哪个对象被锁了)被压入线程私有的 Lock Stack。
3. 卸载时,JVM 把 Lock Stack 的锁记录复制一份到堆内存(虚拟线程对象内部)。
4. 恢复时,从堆内存把锁记录恢复到新载体线程的 Lock Stack。
2、重量级锁:ID 化
Monitor 不再关心是哪个载体线程在干活,它只认虚拟线程的 ID。在 C++ 的 ObjectMonitor 中:
- 旧版:_owner 存的是 JavaThread*(C++ 指针)。
- 新版:_owner 存的是 java.lang.Thread#tid(Java 层的 64 位 ID)。
因为虚拟线程的 tid 是唯一的且持久存在的(即使它被卸载到堆里)。Monitor 只需要对比 ID,就能判断锁是不是被同一个虚拟线程持有(重入),而不需要关心这个虚拟线程此刻挂载在哪个载体线程上。