synchronized的偏向锁、轻量级锁、重量级锁
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 会:
- 在当前线程的栈帧中创建一个名为锁记录的空间。
- 将对象头的 Mark Word 复制到锁记录中。
- 尝试使用 CAS 操作将对象头的 Mark Word 替换为指向该锁记录的指针,并将锁记录中的
owner
指针指向对象头的 Mark Word。
- 如果 CAS 成功:线程成功获取轻量级锁,对象头锁标志位变为
00
。 - 如果 CAS 失败:
- 自旋: 线程不会立即阻塞,而是进行有限次数的自旋(空循环),尝试再次 CAS 获取锁(自适应自旋:JVM 根据上次自旋是否成功动态调整次数)。
- 升级: 如果自旋结束仍未获取到锁(或自旋次数达到阈值),说明存在竞争,锁将膨胀为重量级锁。
- 当线程尝试获取一个处于无锁或偏向锁(但需要撤销)状态的对象锁时,JVM 会:
- 锁释放:
- 使用 CAS 操作尝试将锁记录中的 Mark Word 副本替换回对象头。
- 如果成功:锁释放,对象恢复无锁状态。
- 如果失败:说明锁已膨胀为重量级锁,释放操作会唤醒等待队列中的线程。
- 优点: 在没有实际竞争或竞争很短暂的情况下,避免了操作系统互斥量(mutex)的开销(用户态操作)。
- 缺点: 自旋会消耗 CPU。如果竞争激烈或持有锁时间长,自旋会成为无效开销,不如直接阻塞。
重量级锁 (Heavyweight Locking / Mutex Lock)
- 目标: 处理高竞争场景,即多个线程同时争抢同一把锁。
- 原理:
- 当轻量级锁升级或初始化竞争激烈时,锁膨胀为重量级锁。
- JVM 会向操作系统申请一个互斥量和一个条件变量(通常通过
pthread_mutex_t
和pthread_cond_t
实现)。 - 对象头的 Mark Word 被替换为指向一个 ObjectMonitor 对象(或类似结构,称为监视器或管程)的指针。这个对象内部管理着:
- 互斥锁 (Mutex): 用于控制进入同步块的互斥访问。
- 等待队列 (Wait Set): 调用
wait()
的线程进入此队列。 - 入口队列/竞争队列 (Entry Set / CXQ): 尝试获取锁但未成功的线程进入此队列阻塞等待。
- 拥有者指针 (Owner): 指向当前持有锁的线程。
- 获取锁:
- 线程尝试进入同步块时,通过 CAS 尝试成为 Owner。
- 失败则进入 Entry Set 阻塞等待,由操作系统负责线程调度(涉及用户态到内核态的切换)。
- 释放锁:
- 持有锁的线程退出同步块时,释放 Owner。
- 根据策略(公平/非公平)从 Entry Set 或 Wait Set 中唤醒一个或多个线程竞争锁。
- 优点: 能正确处理高并发竞争,阻塞等待的线程不消耗 CPU。
- 缺点: 性能开销最大。涉及操作系统内核操作(线程阻塞、唤醒、上下文切换),速度慢。
锁升级流程总结
- 初始状态: 对象创建时处于无锁状态。
- 第一次加锁: 线程 A 访问同步块。
- JVM 尝试使用 CAS 将线程 ID 写入 Mark Word(偏向锁)。
- 如果成功,对象进入偏向锁状态,锁标志位
101
,记录线程 A ID。 - 如果失败(或偏向锁禁用),进入下一步。
- 轻量级锁尝试:
- 在 A 的栈帧创建锁记录,复制对象头 Mark Word。
- 尝试用 CAS 将对象头替换为指向锁记录的指针。
- 成功:对象进入轻量级锁状态,锁标志位
00
。 - 失败(说明有竞争,线程 B 也在尝试 CAS):
- 线程 A 自旋重试 CAS。
- 自旋成功:仍为轻量级锁。
- 自旋失败/达到阈值:锁膨胀。
- 锁膨胀 (升级为重量级锁):
- JVM 为对象分配 ObjectMonitor。
- 将对象头 Mark Word 置为指向 ObjectMonitor 的指针,锁标志位
10
。 - 线程 A 和线程 B 都进入重量级锁的竞争流程,由操作系统互斥量管理阻塞和唤醒。
关键点
- 优化方向: 从低开销、应对低竞争(偏向/轻量级)向高开销、应对高竞争(重量级)过渡。
- 性能考量:
- 无/低竞争: 偏向锁 > 轻量级锁 > 重量级锁
- 高竞争: 重量级锁(阻塞不耗CPU) > 轻量级锁(自旋耗CPU)
- 锁只能升级,不能降级(在 HotSpot 中,重量级锁无法降级)。
- 锁消除 (Lock Elimination) 和 锁粗化 (Lock Coarsening) 是另外两种重要的编译器/JVM 优化,与锁状态无关,它们的目标是避免不必要的锁操作。
- 偏向锁的现状: 由于实际收益下降和撤销开销问题,现代 JVM 默认禁用偏向锁。理解其原理仍有价值,但实践中轻量级锁和重量级锁是重点。
理解这些锁状态及其转换机制,对于分析多线程程序的性能瓶颈(如锁竞争导致的延迟、CPU 空转)至关重要。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 技术之路!
评论