“办公室争夺电视遥控器”之锁升级机制

27 阅读6分钟

让我们用一个有趣的“办公室争夺电视遥控器”的故事,并结合代码和时序图,让我们彻底理解Java中的锁升级机制。


一、故事背景:办公室里的电视遥控器(The Lock)

想象一下,在一个开放的办公室里,有一台大家都很爱看的电视。电视只有一个遥控器(共享资源) ,谁拿到遥控器,谁就能换台。

为了保护这个共享资源,避免大家一拥而上导致混乱(数据竞争),行政部制定了一套不断升级的规则来处理争夺情况。这套规则就是 锁升级机制

我们的故事里有几位同事:

  • 线程A:最早来的同事。
  • 线程B:稍晚来的同事。
  • 线程C、D... :更多来看球的同事。
  • 行政部(JVM) :规则的制定和执行者。

锁的几种状态,也就是行政部制定的不同规则:

  1. 无锁状态:没人用电视。
  2. 偏向锁:最近基本上只有一个人看电视。
  3. 轻量级锁:偶尔有两个人同时想换台,但竞争不激烈。
  4. 重量级锁:很多人抢遥控器,场面一度十分混乱。
  5. 自旋优化:轻量级锁失败后,抢锁的人不会立刻放弃,而是在门口转几圈等等看。

二、故事的发展(锁升级流程)

第一幕:天下太平 - 偏向锁 (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,并进入偏向模式。

时序图(偏向锁获取)

Biased.png


第二幕:小小的争执 - 升级为轻量级锁 (Lightweight Locking)

场景:有一天,线程B也想换台看足球。它去拿遥控器,发现标签上写的是“线程A”。

规则(轻量级锁原理)
行政部不会立即升级冲突级别。它这样做:

  1. 行政部暂停了线程A(在安全点),撤销它的偏向锁。
  2. 行政部为线程A和线程B各自创建一份“申请单”Lock Record,锁记录,在线程栈帧中)。
  3. 行政部将遥控器原来的标签(Mark Word)抄录到两份申请单上。
  4. 行政部尝试用CAS操作把遥控器的标签换成一个指针,这个指针指向线程A的申请单(这个操作相当于把遥控器“交给”行政部托管)。
  5. 如果线程A的CAS成功了,那线程A就拿到了轻量级锁,可以继续看电视。
  6. 如果线程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);
    }
}

时序图(升级轻量级锁)

Lightweight .png

自旋优化:线程B CAS失败后,不会立刻被行政部挂起(进入阻塞队列),而是会在门口转几圈(自旋) ,不停地问:“线程A用完了吗?”。如果很快线程A就用完了,线程B就能立即拿到锁,避免了一次耗时的挂起和唤醒操作。如果自旋了一定次数还没拿到,行政部就会介入。


第三幕:场面失控 - 升级为重量级锁 (Heavyweight Locking)

场景:今天有总决赛,线程C、D、E...  一大堆人都来抢遥控器。线程B还在自旋等待,发现线程A一直没释放锁(或者有太多人在自旋)。

规则(重量级锁原理)
行政部认为场面已经失控,轻量级的协商机制顶不住了。于是:

  1. 行政部将规则升级为重量级锁
  2. 它拿出一个官方许可证(Monitor,管程/监视器锁),这个许可证带有一个等待队列
  3. 行政部把遥控器的标签(Mark Word)内容换成一个指向这个许可证(Monitor对象)的指针。
  4. 除了当前正在看电视的线程A(现在持有重量级锁),其他所有线程(B、C、D...)都被请到等待队列里排队,并被挂起(挂起线程,进入BLOCKED状态) ,不再消耗CPU资源自旋。
  5. 当线程A用完遥控器,行政部会通知等待队列里的下一个线程(比如线程B)来获取许可证,并唤醒它。这个挂起和唤醒操作是比较耗时的

代码模拟

// 多个线程激烈竞争
for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        synchronized (lock) { // 大量竞争导致锁升级为重量级锁
            currentChannel++;
            System.out.println("Watching channel " + currentChannel);
        }
    }).start();
}

时序图(升级重量级锁)

Heavyweight .png


三、总结与核心要点

  1. 目的:锁升级的目的是为了减少锁操作的开销。 “非竞争同步”和“低竞争同步”的性能成本尽可能低。如果没有竞争,偏向锁开销极小;如果竞争不激烈,轻量级锁和自旋避免了操作系统挂起线程的开销。
  2. 不可逆:锁的升级路径是单向的:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。一旦升级,就不会降级。因为降级的性能收益通常不如其检查成本高。
  3. Mark Word:这是实现整个机制的核心!它是一个“变脸”大师,在不同锁状态下,存储不同的内容(线程ID、指向Lock Record的指针、指向Monitor的指针等)。它的结构是理解所有优化的钥匙。
  4. CAS操作:这是实现无锁并发和轻量级锁的基础原子操作,是现代CPU提供的硬件支持。
  5. 自适应自旋:JVM会非常智能地学习。如果一个锁之前自旋很快就成功了,JVM可能会允许它下次多自旋一会儿。如果自旋很少成功,JVM可能会直接省略自旋过程,立即升级为重量级锁,以免浪费CPU。

所以,Java的synchronized关键字早已不是昔日的“重量级锁”了。JDK6开始JVM通过这套精巧的锁升级机制,让它在无竞争和低竞争场景下性能表现极佳,同时又能保证高竞争场景下的稳定性。