如果大家对偏向锁有一定了解,可以直接往后看:深入理解Java锁原理(二):轻量级锁的设计原理到实战优化
一、引言
在Java多线程编程中,锁是实现线程安全的重要工具。然而,传统的锁机制(如重量级锁)存在较大的性能开销,尤其是在无竞争的场景下。为了优化这种情况,Java 6引入了偏向锁(Biased Locking),它通过预测锁的使用模式,将无竞争场景下的锁获取和释放成本降为零。本文将深入探讨偏向锁的设计原理、释放机制以及性能优化,帮助开发者更好地理解和使用这一高效的锁机制。
二、偏向锁的核心设计动机
偏向锁的设计基于"锁使用的二八定律":在实际应用中,大部分锁在其生命周期内仅被同一个线程获取,不存在多线程竞争。传统的无锁状态虽然简单,但每次获取锁仍需执行CAS(Compare-and-Swap)操作,而CAS操作虽然轻量,但相比简单的内存比较仍有显著开销。
偏向锁通过消除无竞争场景下的同步原语,进一步提升性能。当同一线程多次获取锁时,偏向锁只需比较Thread ID(一次内存读取操作),而无锁状态仍需执行CAS操作(原子性的读-改-写操作)。
三、偏向锁的实现原理
3.1 对象头与Mark Word
在HotSpot虚拟机中,每个对象的对象头(Object Header)包含两部分信息:Mark Word和Klass Pointer。其中,Mark Word存储了对象的哈希码、分代年龄、锁状态等信息。在64位虚拟机中,Mark Word的结构如下:
当对象处于偏向锁状态时,Mark Word会存储持有锁的线程ID。
3.2 结构解析与关键说明
1. 无锁状态(Normal)
- 布局:25位未使用 + 31位对象哈希码 + 1位未使用 + 4位分代年龄
- 核心特征:
- 哈希码存储在Mark Word中(调用
hashCode()
时生成) - 分代年龄用于GC分代收集
- 锁状态位为
01
(最低两位)
- 哈希码存储在Mark Word中(调用
2. 偏向锁状态(Biased)
- 布局:54位线程ID + 2位epoch(偏向时间戳) + 1位未使用 + 4位分代年龄
- 核心特征:
- 直接存储持有锁的线程ID
- epoch用于标记偏向锁的有效性(避免跨代重用)
- 锁状态位仍为
01
,但通过高位区分偏向模式
3. 轻量级锁状态(Lightweight Locked)
- 布局:62位指向栈中锁记录(Lock Record)的指针
- 核心特征:
- 无锁状态的Mark Word被复制到线程栈帧中
- 通过指针指向锁记录实现加锁
- 锁状态位为
00
4. 重量级锁状态(Heavyweight Locked)
- 布局:62位指向Monitor对象的指针
- 核心特征:
- 指向ObjectMonitor结构体
- 涉及内核态与用户态切换
- 锁状态位为
10
5. GC标记状态(Marked for GC)
- 布局:1位标记 +
01
状态位 - 核心特征:
- 用于GC时的对象标记
- 锁状态位为
11
(仅最低两位有效)
3.3 偏向锁的获取流程
偏向锁的获取流程如下:
从时序图可以看出,当锁已偏向当前线程时,获取锁的操作只需比较Thread ID,无需任何同步操作,成本极低。
四、偏向锁的释放机制
偏向锁的释放机制是其高性能的核心优势之一。与传统锁不同,偏向锁的释放无需任何操作,锁继续保持偏向该线程的状态。
4.1 释放零成本的底层实现
偏向锁的释放无需操作的根本原因在于其"状态持久化"设计:Mark Word中直接存储持有锁的线程ID,锁的"偏向"状态会一直保持,直到发生竞争。当同一个线程再次获取锁时,只需比较Mark Word中的Thread ID是否与当前线程一致,这个比较操作仅需一次内存读取,成本极低。
4.2 释放流程示例
public class BiasedLockExample {
private final Object lock = new Object();
public void method() {
synchronized (lock) { // 首次获取锁:CAS设置偏向锁
// 业务逻辑
} // 释放锁:无需任何操作,锁仍偏向当前线程
synchronized (lock) { // 再次获取锁:仅验证Thread ID
// 快速获取锁,无需同步操作
}
}
}
4.3 与其他锁释放机制的对比
锁类型 | 释放操作 | 成本 |
---|---|---|
偏向锁 | 无操作,锁保持偏向状态 | 零成本 |
轻量级锁 | CAS将Mark Word恢复为原状态 | 一次原子操作 |
重量级锁 | 修改Monitor状态,唤醒EntryList中的线程 | 涉及内核态与用户态切换 |
从对比可以看出,偏向锁在释放锁时的成本为零,这是其在无竞争场景下性能优异的关键原因。
五、偏向锁的竞争与撤销
虽然偏向锁在无竞争场景下性能优异,但当发生锁竞争时,需要进行偏向锁的撤销操作。偏向锁的撤销需要全局安全点(Safe Point),因为撤销操作涉及修改对象头的Mark Word,而其他线程可能正在使用该对象的锁状态。
5.1 撤销流程详解
5.2 为什么需要安全点?
安全点是JVM中的特定位置,此时所有线程的状态是确定的,JVM可以安全地进行内存管理、锁状态修改等操作。在安全点暂停线程的成本较高(需等待所有线程到达安全点),但偏向锁的撤销是罕见操作(仅在第一次竞争时发生),因此整体收益大于成本。
六、偏向锁的优化与权衡
6.1 偏向锁延迟初始化
JVM默认启动时有4秒的偏向锁延迟(-XX:BiasedLockingStartupDelay=0可关闭),因为JVM启动阶段会有大量类加载和静态初始化操作,可能触发不必要的锁竞争。
6.2 批量重偏向与撤销
当一个类的对象频繁发生偏向锁撤销时,JVM会认为该类不适合偏向锁,会批量将该类的对象置为不可偏向状态,避免频繁撤销带来的性能损耗。
6.3 禁用偏向锁
在明确知道锁会被多线程竞争的场景下(如线程池任务),可通过-XX:-UseBiasedLocking禁用偏向锁:
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
synchronized (this) {
// 多线程竞争场景,禁用偏向锁可避免撤销开销
}
});
七、总结
偏向锁通过预测锁的使用模式,将无竞争场景下的锁获取和释放成本降为零,显著提升了单线程或无竞争场景下的性能。其释放无需任何操作的特性,是通过"状态持久化"设计实现的,即锁的偏向状态会一直保持,直到发生竞争。
虽然偏向锁的撤销需要全局安全点,成本较高,但由于撤销是罕见事件,整体性能收益远大于成本。理解偏向锁的设计原理和机制后,开发者可以在设计并发代码时,通过减少锁竞争来充分利用偏向锁的优势,例如使用线程封闭、减少不必要的同步块、优先使用单线程处理模式等。
在实际应用中,应根据具体场景选择合适的锁机制。偏向锁适用于大多数单线程或无竞争的场景,而在竞争激烈的场景下,可能需要考虑使用其他锁机制(如轻量级锁或重量级锁)。通过合理选择和使用锁机制,可以有效提升Java应用的并发性能。