偏向锁(Biased Locking)是Java虚拟机(JVM)为提高单线程环境下的锁性能而引入的一种优化机制。它通过偏向某个线程,避免了无竞争情况下的锁获取和释放开销,进一步提升了同步代码的执行效率。
核心原理
锁对象的偏向状态
当锁对象第一次被线程获取时,JVM会在对象头的Mark Word中记录该线程的ID,称为偏向线程。此后,该线程再次进入同步块时,无需任何同步操作(如CAS或互斥量),直接获得锁。Mark Word的结构变化
- 无锁状态:Mark Word存储哈希码、分代年龄等信息。
- 偏向锁状态:Mark Word存储偏向线程ID、时间戳等信息。
偏向锁的获取与撤销流程
获取锁
- 首次获取锁时,通过CAS操作将线程ID写入对象头Mark Word。
- 成功后,该线程成为偏向线程,后续访问无需任何同步操作。
撤销锁
当有其他线程尝试竞争偏向锁时,偏向锁会被撤销,恢复到无锁状态或升级为轻量级锁。撤销操作需要暂停拥有偏向锁的线程,修改Mark Word,并恢复线程执行。
偏向锁的优缺点
优点
- 无竞争时零开销:单线程重复获取锁的场景下,完全消除了CAS和互斥量的开销。
- 实现简单:仅需修改对象头Mark Word,无需额外的数据结构。
缺点
- 偏向撤销成本高:涉及线程暂停和恢复,在竞争频繁的场景下可能成为性能瓶颈。
- 不适用于多线程竞争:一旦发生竞争,偏向锁的撤销和升级操作会带来额外开销。
偏向锁的适用场景
- 单线程主导的同步块:如Servlet容器处理HTTP请求的线程池。
- 锁竞争极少发生:如初始化阶段的同步操作。
- 锁被同一线程长期持有:如单例模式的双重检查锁定。
Java中的偏向锁示例
以下代码展示了偏向锁在单线程环境下的性能优势:
public class BiasedLockExample {
private static final Object lock = new Object();
private static int counter = 0;
public static void main(String[] args) {
// 预热JVM,触发偏向锁
for (int i = 0; i < 1000; i++) {
synchronized (lock) {
counter++;
}
}
// 单线程重复获取锁(偏向锁生效)
long startTime = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
synchronized (lock) {
counter++;
}
}
long endTime = System.nanoTime();
System.out.println("单线程执行时间: " + (endTime - startTime) / 1000000 + " ms");
System.out.println("Counter: " + counter);
}
}
执行流程:
- 预热阶段:JVM会默认延迟启用偏向锁(约4秒),首次同步块会触发偏向锁的初始化。
- 正式测试:单线程重复获取锁,由于没有竞争,偏向锁无需任何同步操作,执行速度极快。
JVM对偏向锁的优化
偏向锁延迟初始化
JVM启动初期(默认4秒)禁用偏向锁,避免系统初始化阶段的竞争导致频繁撤销。批量重偏向(Bulk Rebias)
当一个对象被多个线程交替访问,但不存在竞争时,JVM会批量重置偏向锁,指向新的线程。批量撤销(Bulk Revoke)
当某个类的对象频繁发生偏向锁撤销时,JVM会将该类所有对象的偏向锁永久撤销,直接升级为轻量级锁。
偏向锁的配置参数
通过JVM参数可以调整偏向锁的行为:
-XX:+UseBiasedLocking
:启用偏向锁(默认开启,JDK 15+默认关闭)。-XX:BiasedLockingStartupDelay=0
:取消偏向锁启动延迟。-XX:-UseBiasedLocking
:禁用偏向锁,所有锁直接升级为轻量级锁。
偏向锁与其他锁的对比
锁类型 | 适用场景 | 锁获取开销 | 锁释放开销 | 竞争处理方式 |
---|---|---|---|---|
偏向锁 | 单线程重复获取锁 | 首次CAS,后续0 | 0 | 撤销并升级 |
轻量级锁 | 多线程交替获取锁 | CAS | CAS | 升级为重量级锁 |
重量级锁 | 多线程同时竞争锁 | 内核互斥量 | 内核互斥量 | 线程阻塞 |
总结
偏向锁是JVM针对单线程场景的极致优化,通过消除无竞争情况下的同步开销,显著提升了性能。但在多线程竞争频繁的场景下,偏向锁的撤销和升级操作可能成为性能瓶颈,此时应考虑禁用偏向锁或使用其他并发工具。理解偏向锁的工作原理有助于优化高并发系统的性能。