Java 中的 synchronized 关键字是实现线程同步的核心机制。为了在不同竞争程度的场景下优化性能,JVM 采用了锁升级策略。对象锁的状态会随着竞争情况的变化,从无锁状态逐步升级:偏向锁 -> 轻量级锁 -> 重量级锁

这些锁的状态信息存储在 Java 对象头的 Mark Word 中。


偏向锁 (Biased Locking)

  • 目标: 优化同一个线程多次获取同一把锁的场景(无实际竞争或极少竞争)。
  • 原理:
    • 当一个线程第一次获得锁时,JVM 会在对象头的 Mark Word 中记录该线程的 ID(偏向线程ID)以及一个偏向时间戳(epoch)。
    • 之后该线程再次进入这个同步块时,不需要进行任何同步操作(如 CAS、系统调用),只需简单检查 Mark Word 中的线程 ID 是否指向自己:
      • 如果是:直接执行。
      • 如果不是:说明存在竞争(即使另一个线程已经释放了锁,偏向也不会自动撤销),此时需要撤销偏向锁
  • 撤销偏向锁:
    • 需要等到全局安全点(此时 JVM 暂停所有工作线程)。
    • 检查持有偏向锁的线程是否还活着或是否仍在同步块内。
    • 如果线程已不存活或不在同步块内:将对象头置为无锁状态(或根据情况升级为轻量级锁)。
    • 如果线程仍在同步块内:升级为轻量级锁,并将 Mark Word 复制到该线程栈的锁记录(Lock Record)中,然后用指向 Lock Record 的指针 CAS 更新 Mark Word。
  • 优点: 对于无竞争或线程内重入的情况,性能极高,几乎没有同步开销。
  • 缺点: 撤销操作(尤其在非活动线程持有偏向锁时)有一定开销。如果存在明显竞争,偏向锁反而会降低性能。
  • 现状: 自 JDK 15 起,偏向锁默认被禁用且在未来版本中计划移除。主要原因是其收益在现代应用(尤其大量使用短生命周期对象的框架)中变得不明显,且撤销开销有时成为负担。可通过 JVM 参数 -XX:+UseBiasedLocking 重新启用(但不推荐)。

轻量级锁 (Lightweight Locking / Thin Lock)

  • 目标: 优化多个线程交替执行同步块,没有真正并发竞争(即竞争程度很低,线程不会在同一个时间点同时争抢锁)的场景。
  • 原理:
    • 当线程尝试获取一个处于无锁或偏向锁(但需要撤销)状态的对象锁时,JVM 会:
      1. 在当前线程的栈帧中创建一个名为锁记录的空间。
      2. 将对象头的 Mark Word 复制到锁记录中。
      3. 尝试使用 CAS 操作将对象头的 Mark Word 替换为指向该锁记录的指针,并将锁记录中的 owner 指针指向对象头的 Mark Word。
    • 如果 CAS 成功:线程成功获取轻量级锁,对象头锁标志位变为 00
    • 如果 CAS 失败:
      • 自旋: 线程不会立即阻塞,而是进行有限次数的自旋(空循环),尝试再次 CAS 获取锁(自适应自旋:JVM 根据上次自旋是否成功动态调整次数)。
      • 升级: 如果自旋结束仍未获取到锁(或自旋次数达到阈值),说明存在竞争,锁将膨胀为重量级锁。
  • 锁释放:
    • 使用 CAS 操作尝试将锁记录中的 Mark Word 副本替换回对象头。
    • 如果成功:锁释放,对象恢复无锁状态。
    • 如果失败:说明锁已膨胀为重量级锁,释放操作会唤醒等待队列中的线程。
  • 优点: 在没有实际竞争或竞争很短暂的情况下,避免了操作系统互斥量(mutex)的开销(用户态操作)。
  • 缺点: 自旋会消耗 CPU。如果竞争激烈或持有锁时间长,自旋会成为无效开销,不如直接阻塞。

重量级锁 (Heavyweight Locking / Mutex Lock)

  • 目标: 处理高竞争场景,即多个线程同时争抢同一把锁。
  • 原理:
    • 当轻量级锁升级或初始化竞争激烈时,锁膨胀为重量级锁。
    • JVM 会向操作系统申请一个互斥量和一个条件变量(通常通过 pthread_mutex_tpthread_cond_t 实现)。
    • 对象头的 Mark Word 被替换为指向一个 ObjectMonitor 对象(或类似结构,称为监视器或管程)的指针。这个对象内部管理着:
      • 互斥锁 (Mutex): 用于控制进入同步块的互斥访问。
      • 等待队列 (Wait Set): 调用 wait() 的线程进入此队列。
      • 入口队列/竞争队列 (Entry Set / CXQ): 尝试获取锁但未成功的线程进入此队列阻塞等待。
      • 拥有者指针 (Owner): 指向当前持有锁的线程。
  • 获取锁:
    • 线程尝试进入同步块时,通过 CAS 尝试成为 Owner。
    • 失败则进入 Entry Set 阻塞等待,由操作系统负责线程调度(涉及用户态到内核态的切换)。
  • 释放锁:
    • 持有锁的线程退出同步块时,释放 Owner。
    • 根据策略(公平/非公平)从 Entry Set 或 Wait Set 中唤醒一个或多个线程竞争锁。
  • 优点: 能正确处理高并发竞争,阻塞等待的线程不消耗 CPU。
  • 缺点: 性能开销最大。涉及操作系统内核操作(线程阻塞、唤醒、上下文切换),速度慢。

锁升级流程总结

  1. 初始状态: 对象创建时处于无锁状态。
  2. 第一次加锁: 线程 A 访问同步块。
    • JVM 尝试使用 CAS 将线程 ID 写入 Mark Word(偏向锁)。
    • 如果成功,对象进入偏向锁状态,锁标志位 101,记录线程 A ID。
    • 如果失败(或偏向锁禁用),进入下一步。
  3. 轻量级锁尝试:
    • 在 A 的栈帧创建锁记录,复制对象头 Mark Word。
    • 尝试用 CAS 将对象头替换为指向锁记录的指针。
    • 成功:对象进入轻量级锁状态,锁标志位 00
    • 失败(说明有竞争,线程 B 也在尝试 CAS):
      • 线程 A 自旋重试 CAS。
      • 自旋成功:仍为轻量级锁。
      • 自旋失败/达到阈值:锁膨胀
  4. 锁膨胀 (升级为重量级锁):
    • JVM 为对象分配 ObjectMonitor。
    • 将对象头 Mark Word 置为指向 ObjectMonitor 的指针,锁标志位 10
    • 线程 A 和线程 B 都进入重量级锁的竞争流程,由操作系统互斥量管理阻塞和唤醒。

关键点

  • 优化方向: 从低开销、应对低竞争(偏向/轻量级)向高开销、应对高竞争(重量级)过渡。
  • 性能考量:
    • 无/低竞争: 偏向锁 > 轻量级锁 > 重量级锁
    • 高竞争: 重量级锁(阻塞不耗CPU) > 轻量级锁(自旋耗CPU)
  • 锁只能升级,不能降级(在 HotSpot 中,重量级锁无法降级)。
  • 锁消除 (Lock Elimination)锁粗化 (Lock Coarsening) 是另外两种重要的编译器/JVM 优化,与锁状态无关,它们的目标是避免不必要的锁操作。
  • 偏向锁的现状: 由于实际收益下降和撤销开销问题,现代 JVM 默认禁用偏向锁。理解其原理仍有价值,但实践中轻量级锁和重量级锁是重点。

理解这些锁状态及其转换机制,对于分析多线程程序的性能瓶颈(如锁竞争导致的延迟、CPU 空转)至关重要。