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