锁升级机制与重量锁底层原理
在 JDK 1.6 之前,synchronized 是一个重量级锁,性能较低。为了减少获得锁和释放锁带来的性能消耗,JDK 1.6 对其进行了重大优化,引入了偏向锁和轻量级锁。
锁的升级过程是不可逆的(在某些特殊情况下如 GC 可能会发生降级,但主流理解为不可逆):无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。
这一切的基础都建立在 Java 对象头中的 Mark Word 之上。
锁升级机制
0. 基础知识:Mark Word 的结构
在 64 位 JVM 中,Mark Word 占 8 字节(64位)。锁的状态通过最后 2 位(或 3 位,含偏向标志位)来标识:
- 001:无锁
- 101:偏向锁
- 00:轻量级锁
- 10:重量级锁
第一步:无锁 -> 偏向锁 (Biased Locking)
核心思想:锁总是由同一线程多次获得,为了让该线程获取锁的代价更低,引入偏向锁。
线程操作流程:
- 初始状态:对象刚创建,Mark Word 后三位是
101(开启了偏向锁),但 Thread ID 为空。 - 首次获取:线程 A 到达。它发现是偏向锁状态,通过 CAS 操作尝试将自己的
Thread ID写入 Mark Word。 - 操作成功:CAS 成功,Mark Word 现在记录了线程 A 的 ID。线程 A 以后每次进入该同步块,只需简单检查 Thread ID 是否是自己,无需 CAS,无需内核态切换。
- 释放锁:从栈帧中弹出 Lock Record。
第二步:偏向锁 -> 轻量级锁 (Lightweight Locking)
触发条件:出现了竞争。当另一个线程(线程 B)尝试获取锁时,偏向锁模式宣告结束。
线程操作流程:
- 撤销偏向:当线程 B 尝试获取锁,发现 Mark Word 里是线程 A 的 ID。JVM 会在一个 全局安全点(Safepoint) 挂起线程 A。
- 状态转换:
-
如果线程 A 执行完同步块,置为无锁状态进行重新偏向
-
如果线程 A 还在执行同步块,则将偏向锁升级为轻量级锁。
-
- 轻量级锁加锁细节:
- 每个线程在自己的栈帧中创建空间,称为“锁记录”(Lock Record)。
- 线程将对象头中的 Mark Word 复制到自己的 Lock Record 中(称为 Displaced Mark Word)。
- 线程尝试通过 CAS 将对象头的 Mark Word 替换为指向自己栈中 Lock Record 的指针。
- 操作结果:
- 成功:当前线程获得锁,Mark Word 状态变为
00。 - 失败:说明有其他线程竞争。
- 成功:当前线程获得锁,Mark Word 状态变为
值得注意的是,从偏向锁升级为轻量级锁的损耗是巨大的,要将所有的线程全部挂起,所以JVM默认开启的偏向模式是一种延时模式,等待一段时间后再开启偏向锁,在这个期间,大量线程竞争资源会导致,其从无锁升级为轻量级锁。JDK15直接将其废除,现代 JVM 更倾向于直接从无锁进入轻量级锁。
第三步:轻量级锁 -> 重量级锁 (Heavyweight Locking)
核心思想:轻量级锁假设“竞争是短暂的”。如果竞争激烈,长时间自旋会浪费 CPU。
线程操作流程:
- 自旋等待:当线程 B 尝试获取轻量级锁失败时(CAS 失败),它不会立即阻塞,而是执行自旋(循环尝试获取锁)。
- 自旋失败:
- 在 JDK 1.6 中引入了自适应自旋(Adaptive Spinning)。如果自旋超过一定次数,或者又有第三个线程来竞争。
- 膨胀为重量级锁:
- 锁状态变为
10。 - Mark Word 中存储的是指向 Monitor(管程) 对象的指针。
- 线程 B 及其后面来的所有线程都会被阻塞(Blocked)。
- 锁状态变为
- 重量级锁操作:
- 底层依赖操作系统的
Mutex Lock(互斥量)实现。 - 当线程 A 执行完释放锁时,它需要唤醒在等待队列中的线程。
- 线程切换开销大:涉及用户态到内核态的切换。
- 底层依赖操作系统的
总结:线程操作对比图
| 锁类型 | 核心操作 | 线程竞争时的行为 | 优点 | 缺点 |
|---|---|---|---|---|
| 偏向锁 | 对比 Thread ID | 撤销偏向锁,升级 | 几乎无开销(只有一个线程时) | 如果锁竞争激烈,频繁撤销会有性能开销 |
| 轻量级锁 | CAS 替换指针 | 自旋尝试获取 | 竞争失败不立即阻塞,响应快 | 若自旋长时间拿不到锁,白白消耗 CPU |
| 重量级锁 | OS 互斥量 | 阻塞,进入等待队列 | 不消耗 CPU(线程挂起) | 线程上下文切换开销巨大 |
重量级锁底层原理:
在Java中,synchronized 关键字的底层实现经历过重大的优化,引入了“锁升级”的机制(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁)。
重量级锁(Heavyweight Lock) 是 synchronized 锁升级的最终状态。当多个线程发生激烈的锁竞争时,轻量级锁就会膨胀为重量级锁。
以下是关于 synchronized 重量级锁机制的详细原理解析:
1. 为什么叫“重量级”?
重量级锁的“重”,体现在它依赖于操作系统的互斥量(Mutex Lock)来实现。
在Java中,线程是映射到操作系统的原生线程上的。当一个线程获取不到重量级锁时,它会被阻塞(挂起),等待锁释放后被唤醒。
- 状态切换开销大: 阻塞和唤醒线程需要操作系统介入,这意味着需要从用户态(User Mode)切换到内核态(Kernel Mode)。
- 上下文切换耗时: 这种状态转换和线程上下文切换的成本非常高,往往需要消耗几万个CPU时钟周期,甚至比同步代码块自身的执行时间还要长。
2. 重量级锁的触发时机
- 轻量级锁自旋失败: 当一个线程尝试获取轻量级锁失败,它会进行自旋(空循环等待)。如果自旋达到一定次数(或者自旋期间又有第三个线程来竞争),为了不浪费CPU资源,锁就会膨胀为重量级锁。
- 直接调用
wait()方法: 如果在同步块中调用了对象的wait()方法,因为wait()需要依赖重量级锁的核心数据结构(WaitSet),锁会直接膨胀为重量级锁。
3. 核心机制:ObjectMonitor(对象监视器)
当对象变成重量级锁时,Java对象头(Mark Word)中的指针会直接指向一个底层C++实现的对象——ObjectMonitor(对象监视器)。
每个Java对象都可以关联一个 ObjectMonitor。它内部有几个核心的属性(C++源码级别):
_owner:指向当前持有该锁的线程。如果为 null,表示当前没有线程占用锁。_cxq(Contention Queue):竞争队列。所有请求获取锁但失败的线程,首先会被放入这个单向链表中。_EntryList:就绪队列。当锁被释放时,_owner会从_cxq或_EntryList中唤醒线程来竞争锁。通常,_cxq中的线程会被转移到_EntryList中。_WaitSet:等待集合。处于wait状态的线程会被放入这个双向链表中。只有当被notify()或notifyAll()唤醒时,它们才会被转移到_cxq或_EntryList中重新竞争锁。_recursions:记录锁的重入次数。
4. 重量级锁的运行流程(生命周期)
A. 加锁过程 (Enter)
- 线程尝试获取锁,发现对象头指向的是重量级锁(
ObjectMonitor)。 - 线程尝试通过 CAS 操作将
ObjectMonitor的_owner字段设置为当前线程。 - 如果设置成功,说明获取到了锁,
_recursions= 1,继续执行同步代码。 - 如果设置失败(
_owner已经被别的线程占用):- 如果
_owner就是当前线程,说明是重入,_recursions加 1,继续执行。 - 如果是被其他线程占用,当前线程会被封装成一个节点,加入到
_cxq队列 中,然后调用操作系统的park()方法将当前线程挂起(阻塞)。
- 如果
B. 等待/唤醒过程 (Wait / Notify)
wait(): 当持有锁的线程调用wait()时,它会释放锁(_owner置空,_recursions清零),然后把自己加入到_WaitSet队列中,并挂起自己。此时,它会唤醒_EntryList中的下一个线程。notify(): 当持有锁的线程调用notify()时,它会从_WaitSet中取出一个线程,将其转移到_EntryList或_cxq中,使其具备重新竞争锁的资格(注意:此时持有锁的线程并没有释放锁,被唤醒的线程还需要等锁释放后才能真正运行)。
C. 解锁过程 (Exit)
- 线程执行完同步代码块,或者抛出异常。
- 将
ObjectMonitor的_recursions减 1。 - 如果减 1 后
_recursions仍然大于 0,说明还在重入层级中,不释放锁。 - 如果
_recursions等于 0,说明完全退出,将_owner置为 null。 - 此时,释放锁的线程会检查
_EntryList和_cxq。如果不为空,它会调用系统的unpark()方法,唤醒队列中的一个线程,让它去竞争锁。
5. 重量级锁的优缺点
- 优点:
- 不会消耗 CPU 资源进行无意义的空转(自旋)。当线程获取不到锁时,直接挂起,把 CPU 让给其他线程。
- 非常适合同步代码块执行时间较长、锁竞争非常激烈的场景。
- 缺点:
- 加锁和解锁需要借助操作系统的 Mutex Lock,发生用户态和内核态的切换,开销巨大。
- 如果同步代码块执行极快,线程刚被挂起又要被唤醒,系统的调度成本会远高于代码本身的执行成本。
总结
synchronized 的重量级锁本质上是基于管程(Monitor)模型和操作系统互斥量实现的。它的核心是 ObjectMonitor 对象,通过内部的 _owner 标识所有者,用 _EntryList 和 _cxq 存放阻塞线程,用 _WaitSet 处理 wait/notify 机制。由于涉及线程的挂起、唤醒和内核态切换,它是 synchronized 性能最低但最能应对高并发竞争的状态。
更多推荐



所有评论(0)