让我们用一个有趣的“办公室争夺电视遥控器”的故事,并结合代码和时序图,让我们彻底理解Java中的锁升级机制。
一、故事背景:办公室里的电视遥控器(The Lock)
想象一下,在一个开放的办公室里,有一台大家都很爱看的电视。电视只有一个遥控器(共享资源) ,谁拿到遥控器,谁就能换台。
为了保护这个共享资源,避免大家一拥而上导致混乱(数据竞争),行政部制定了一套不断升级的规则来处理争夺情况。这套规则就是 锁升级机制。
我们的故事里有几位同事:
- 线程A:最早来的同事。
- 线程B:稍晚来的同事。
- 线程C、D... :更多来看球的同事。
- 行政部(JVM) :规则的制定和执行者。
锁的几种状态,也就是行政部制定的不同规则:
- 无锁状态:没人用电视。
- 偏向锁:最近基本上只有一个人看电视。
- 轻量级锁:偶尔有两个人同时想换台,但竞争不激烈。
- 重量级锁:很多人抢遥控器,场面一度十分混乱。
- 自旋优化:轻量级锁失败后,抢锁的人不会立刻放弃,而是在门口转几圈等等看。
二、故事的发展(锁升级流程)
第一幕:天下太平 - 偏向锁 (Biased Locking)
场景:最近几个月,几乎只有线程A一个人中午看新闻。行政部觉得每次都走完整的申请流程太麻烦了。
规则(偏向锁原理) :
行政部在遥控器上贴了一张标签(Mark Word),上面写着:“默认归属:线程A”。以后只要线程A来拿遥控器,行政部一看标签是他,就直接把遥控器给他了,连登记都省了。这个过程非常快,因为只是一次简单的CAS(Compare-And-Swap) 操作,比较并交换标签。
代码模拟:
public class TVRoom {
private final Object lock = new Object(); // 这就是遥控器
private int currentChannel = 1; // 共享资源:电视频道
public void watchNews() {
synchronized (lock) { // 线程A第一次进入同步块,启用偏向锁
currentChannel = 13; // CCTV-新闻
System.out.println("Watching news on channel " + currentChannel);
}
}
}
- 此时,
lock
对象头中的Mark Word记录了线程A的ID,并进入偏向模式。
时序图(偏向锁获取) :
第二幕:小小的争执 - 升级为轻量级锁 (Lightweight Locking)
场景:有一天,线程B也想换台看足球。它去拿遥控器,发现标签上写的是“线程A”。
规则(轻量级锁原理) :
行政部不会立即升级冲突级别。它这样做:
- 行政部暂停了线程A(在安全点),撤销它的偏向锁。
- 行政部为线程A和线程B各自创建一份“申请单” (Lock Record,锁记录,在线程栈帧中)。
- 行政部将遥控器原来的标签(Mark Word)抄录到两份申请单上。
- 行政部尝试用CAS操作把遥控器的标签换成一个指针,这个指针指向线程A的申请单(这个操作相当于把遥控器“交给”行政部托管)。
- 如果线程A的CAS成功了,那线程A就拿到了轻量级锁,可以继续看电视。
- 如果线程B也同时尝试用CAS把标签指向自己的申请单,那么只会有一个成功(CAS的原子性保证),另一个失败。
代码模拟:
public void threadAMethod() {
synchronized (lock) { // 此时偏向锁被撤销,准备升级为轻量级锁
currentChannel = 13;
}
}
public void threadBMethod() {
synchronized (lock) { // 线程B也尝试获取锁,发生竞争
currentChannel = 5; // CCTV-体育
System.out.println("Watching soccer on channel " + currentChannel);
}
}
时序图(升级轻量级锁) :
自旋优化:线程B CAS失败后,不会立刻被行政部挂起(进入阻塞队列),而是会在门口转几圈(自旋) ,不停地问:“线程A用完了吗?”。如果很快线程A就用完了,线程B就能立即拿到锁,避免了一次耗时的挂起和唤醒操作。如果自旋了一定次数还没拿到,行政部就会介入。
第三幕:场面失控 - 升级为重量级锁 (Heavyweight Locking)
场景:今天有总决赛,线程C、D、E... 一大堆人都来抢遥控器。线程B还在自旋等待,发现线程A一直没释放锁(或者有太多人在自旋)。
规则(重量级锁原理) :
行政部认为场面已经失控,轻量级的协商机制顶不住了。于是:
- 行政部将规则升级为重量级锁。
- 它拿出一个官方许可证(Monitor,管程/监视器锁),这个许可证带有一个等待队列。
- 行政部把遥控器的标签(Mark Word)内容换成一个指向这个许可证(Monitor对象)的指针。
- 除了当前正在看电视的线程A(现在持有重量级锁),其他所有线程(B、C、D...)都被请到等待队列里排队,并被挂起(挂起线程,进入BLOCKED状态) ,不再消耗CPU资源自旋。
- 当线程A用完遥控器,行政部会通知等待队列里的下一个线程(比如线程B)来获取许可证,并唤醒它。这个挂起和唤醒操作是比较耗时的。
代码模拟:
// 多个线程激烈竞争
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (lock) { // 大量竞争导致锁升级为重量级锁
currentChannel++;
System.out.println("Watching channel " + currentChannel);
}
}).start();
}
时序图(升级重量级锁) :
三、总结与核心要点
- 目的:锁升级的目的是为了减少锁操作的开销。 “非竞争同步”和“低竞争同步”的性能成本尽可能低。如果没有竞争,偏向锁开销极小;如果竞争不激烈,轻量级锁和自旋避免了操作系统挂起线程的开销。
- 不可逆:锁的升级路径是单向的:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。一旦升级,就不会降级。因为降级的性能收益通常不如其检查成本高。
- Mark Word:这是实现整个机制的核心!它是一个“变脸”大师,在不同锁状态下,存储不同的内容(线程ID、指向Lock Record的指针、指向Monitor的指针等)。它的结构是理解所有优化的钥匙。
- CAS操作:这是实现无锁并发和轻量级锁的基础原子操作,是现代CPU提供的硬件支持。
- 自适应自旋:JVM会非常智能地学习。如果一个锁之前自旋很快就成功了,JVM可能会允许它下次多自旋一会儿。如果自旋很少成功,JVM可能会直接省略自旋过程,立即升级为重量级锁,以免浪费CPU。
所以,Java的synchronized
关键字早已不是昔日的“重量级锁”了。JDK6开始JVM通过这套精巧的锁升级机制,让它在无竞争和低竞争场景下性能表现极佳,同时又能保证高竞争场景下的稳定性。