在 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)

核心思想:锁总是由同一线程多次获得,为了让该线程获取锁的代价更低,引入偏向锁。

线程操作流程:
  1. 初始状态:对象刚创建,Mark Word 后三位是 101(开启了偏向锁),但 Thread ID 为空。
  2. 首次获取:线程 A 到达。它发现是偏向锁状态,通过 CAS 操作尝试将自己的 Thread ID 写入 Mark Word。
  3. 操作成功:CAS 成功,Mark Word 现在记录了线程 A 的 ID。线程 A 以后每次进入该同步块,只需简单检查 Thread ID 是否是自己,无需 CAS,无需内核态切换
  4. 释放锁:从栈帧中弹出 Lock Record。

第二步:偏向锁 -> 轻量级锁 (Lightweight Locking)

触发条件:出现了竞争。当另一个线程(线程 B)尝试获取锁时,偏向锁模式宣告结束。

线程操作流程:
  1. 撤销偏向:当线程 B 尝试获取锁,发现 Mark Word 里是线程 A 的 ID。JVM 会在一个 全局安全点(Safepoint) 挂起线程 A。
  2. 状态转换
    • 如果线程 A 执行完同步块,置为无锁状态进行重新偏向

    • 如果线程 A 还在执行同步块,则将偏向锁升级为轻量级锁。

  3. 轻量级锁加锁细节
    • 每个线程在自己的栈帧中创建空间,称为“锁记录”(Lock Record)。
    • 线程将对象头中的 Mark Word 复制到自己的 Lock Record 中(称为 Displaced Mark Word)。
    • 线程尝试通过 CAS 将对象头的 Mark Word 替换为指向自己栈中 Lock Record 的指针。
  4. 操作结果
    • 成功:当前线程获得锁,Mark Word 状态变为 00
    • 失败:说明有其他线程竞争。

值得注意的是,从偏向锁升级为轻量级锁的损耗是巨大的,要将所有的线程全部挂起,所以JVM默认开启的偏向模式是一种延时模式,等待一段时间后再开启偏向锁,在这个期间,大量线程竞争资源会导致,其从无锁升级为轻量级锁。JDK15直接将其废除,现代 JVM 更倾向于直接从无锁进入轻量级锁。


第三步:轻量级锁 -> 重量级锁 (Heavyweight Locking)

核心思想:轻量级锁假设“竞争是短暂的”。如果竞争激烈,长时间自旋会浪费 CPU。

线程操作流程:
  1. 自旋等待:当线程 B 尝试获取轻量级锁失败时(CAS 失败),它不会立即阻塞,而是执行自旋(循环尝试获取锁)。
  2. 自旋失败
    • 在 JDK 1.6 中引入了自适应自旋(Adaptive Spinning)。如果自旋超过一定次数,或者又有第三个线程来竞争。
  3. 膨胀为重量级锁
    • 锁状态变为 10
    • Mark Word 中存储的是指向 Monitor(管程) 对象的指针。
    • 线程 B 及其后面来的所有线程都会被阻塞(Blocked)
  4. 重量级锁操作
    • 底层依赖操作系统的 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)
  1. 线程尝试获取锁,发现对象头指向的是重量级锁(ObjectMonitor)。
  2. 线程尝试通过 CAS 操作将 ObjectMonitor_owner 字段设置为当前线程。
  3. 如果设置成功,说明获取到了锁,_recursions = 1,继续执行同步代码。
  4. 如果设置失败(_owner 已经被别的线程占用):
    • 如果 _owner 就是当前线程,说明是重入_recursions 加 1,继续执行。
    • 如果是被其他线程占用,当前线程会被封装成一个节点,加入到 _cxq 队列 中,然后调用操作系统的 park() 方法将当前线程挂起(阻塞)
B. 等待/唤醒过程 (Wait / Notify)
  • wait() 当持有锁的线程调用 wait() 时,它会释放锁(_owner 置空,_recursions 清零),然后把自己加入到 _WaitSet 队列中,并挂起自己。此时,它会唤醒 _EntryList 中的下一个线程。
  • notify() 当持有锁的线程调用 notify() 时,它会从 _WaitSet 中取出一个线程,将其转移到 _EntryList_cxq 中,使其具备重新竞争锁的资格(注意:此时持有锁的线程并没有释放锁,被唤醒的线程还需要等锁释放后才能真正运行)。
C. 解锁过程 (Exit)
  1. 线程执行完同步代码块,或者抛出异常。
  2. ObjectMonitor_recursions 减 1。
  3. 如果减 1 后 _recursions 仍然大于 0,说明还在重入层级中,不释放锁。
  4. 如果 _recursions 等于 0,说明完全退出,将 _owner 置为 null
  5. 此时,释放锁的线程会检查 _EntryList_cxq。如果不为空,它会调用系统的 unpark() 方法,唤醒队列中的一个线程,让它去竞争锁。

5. 重量级锁的优缺点

  • 优点:
    • 不会消耗 CPU 资源进行无意义的空转(自旋)。当线程获取不到锁时,直接挂起,把 CPU 让给其他线程。
    • 非常适合同步代码块执行时间较长锁竞争非常激烈的场景。
  • 缺点:
    • 加锁和解锁需要借助操作系统的 Mutex Lock,发生用户态和内核态的切换,开销巨大。
    • 如果同步代码块执行极快,线程刚被挂起又要被唤醒,系统的调度成本会远高于代码本身的执行成本。

总结

synchronized 的重量级锁本质上是基于管程(Monitor)模型操作系统互斥量实现的。它的核心是 ObjectMonitor 对象,通过内部的 _owner 标识所有者,用 _EntryList_cxq 存放阻塞线程,用 _WaitSet 处理 wait/notify 机制。由于涉及线程的挂起、唤醒和内核态切换,它是 synchronized 性能最低但最能应对高并发竞争的状态。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐