在阅读本章前,需读者对偏向锁有一个较为深刻的了解:深入理解Java锁原理(一):偏向锁的设计原理与性能优化
一、引言
在Java多线程编程中,锁是实现线程安全的重要工具。然而,传统的重量级锁(如synchronized)存在较大的性能开销,尤其是在无竞争或轻度竞争的场景下。为了优化这种情况,Java 6引入了轻量级锁(Lightweight Locking),它通过CAS操作和自旋等待来避免线程阻塞,显著提升了并发性能。本文将深入探讨轻量级锁的设计原理、实现机制以及在竞争场景下的行为,帮助开发者更好地理解和使用这一高效的锁机制。
二、轻量级锁的核心设计思想
轻量级锁的设计目标是在无竞争或轻度竞争的场景下,避免使用重量级锁带来的线程阻塞和上下文切换开销。它的核心思想是:在竞争不激烈时,通过CAS操作和自旋等待实现快速加锁,只有在竞争激烈时才升级为重量级锁。
与偏向锁和重量级锁的对比:
锁类型 | 竞争程度 | 核心实现机制 | 典型耗时 |
---|---|---|---|
偏向锁 | 无竞争 | Mark Word记录线程ID | 纳秒级(无需CAS) |
轻量级锁 | 轻度竞争 | CAS操作+栈帧锁记录 | 10-100纳秒 |
重量级锁 | 重度竞争 | ObjectMonitor+线程阻塞 | 毫秒级(上下文切换) |
三、轻量级锁的实现机制
3.1 锁记录(Lock Record)的创建
每个线程的栈帧中都有一个用于存储锁记录的空间(LocalLockRecord),其核心结构:
// 伪代码表示栈帧锁记录
class LocalLockRecord {
Object reference; // 指向锁对象
markOop markWord; // 保存锁对象的原Mark Word
// 其他线程私有数据
}
3.2 轻量级锁的加锁流程
当线程尝试获取轻量级锁时,会执行以下步骤:
- 在当前线程的栈帧中创建一个锁记录(Lock Record)。
- 将锁对象的Mark Word复制到锁记录中(此时Mark Word可能是无锁状态或偏向锁状态)。
- 尝试用CAS操作将锁对象的Mark Word替换为指向锁记录的指针。
- 如果CAS成功,当前线程获得轻量级锁。
- 如果CAS失败,表示有其他线程竞争该锁,当前线程会尝试自旋等待锁释放,或者升级为重量级锁。
3.3 轻量级锁的解锁流程
轻量级锁的解锁过程同样基于CAS操作:
- 线程尝试用CAS将栈记录中的原Mark Word还原到锁对象的Mark Word。
- 如果CAS成功,锁对象恢复到无锁状态,解锁完成。
- 如果CAS失败,表示有其他线程在竞争该锁,此时会升级为重量级锁。
// 轻量级锁解锁伪代码
void unlock(Object obj) {
LocalLockRecord lr = getLockRecord();
markOop expectedMark = lr.markWord;
markOop newMark = obj.mark();
if (newMark == lr.lockRecordPointer) {
// 尝试还原原Mark Word
if (CAS(obj.mark(), lr.lockRecordPointer, expectedMark)) {
return; // 解锁成功
}
}
// CAS失败,进入重量级锁膨胀流程
inflateToHeavyweightLock(obj, lr);
}
四、偏向锁升级到轻量级锁的过程
当第二个线程尝试获取偏向锁时,会触发偏向锁的撤销并升级为轻量级锁。这个过程比较复杂,需要分步骤说明:
4.1 偏向锁释放与Mark Word状态
偏向锁释放后,Mark Word不会恢复到无锁状态,而是保持偏向状态直到发生竞争。当有其他线程尝试获取该锁时,才会触发偏向锁的撤销。
// 偏向锁释放过程示例
public void method() {
synchronized (lock) {
// 偏向锁获取:Mark Word存储线程ID
} // 偏向锁释放:Mark Word保持不变,仍存储线程ID
} // 此时Mark Word仍是偏向锁状态,而非无锁状态
4.2 偏向锁升级为轻量级锁的详细流程
五、轻量级锁的竞争检测机制
当其他线程尝试获取已经升级为轻量级锁的对象时,通过以下步骤检测锁状态:
- 读取锁对象的Mark Word,发现其指向某个线程的Lock Record(轻量级锁状态)。
- 判断Lock Record是否属于当前线程:
- 如果属于当前线程,则当前线程已持有锁,直接进入同步块。
- 如果属于其他线程,则当前线程未持有锁,进入竞争逻辑。
- 竞争处理:当前线程会先尝试自旋等待锁释放,若自旋失败则触发锁膨胀(升级为重量级锁)。
// 轻量级锁竞争检测伪代码
public void acquireLock(Object lock) {
// 1. 读取锁对象的Mark Word
markOop mark = lock.getMarkWord();
// 2. 判断是否为轻量级锁状态
if (mark.isLightweightLocked()) {
// 3. 获取Mark Word指向的Lock Record
LockRecord lockRecord = mark.getLockRecord();
// 4. 判断Lock Record是否属于当前线程
if (lockRecord.getOwner() == Thread.currentThread()) {
// 锁重入,直接获取锁
return;
} else {
// 锁被其他线程持有,尝试自旋竞争
for (int i = 0; i < MAX_SPIN_TIMES; i++) {
if (tryLockCAS(lock)) {
return; // 自旋成功获取锁
}
Thread.yield(); // 短暂让出CPU
}
// 自旋失败,升级为重量级锁
inflateToHeavyweightLock(lock);
}
}
}
六、轻量级锁的自旋优化
轻量级锁在加锁失败时不会立即阻塞线程,而是通过自旋(Spin)尝试再次获取锁。自旋的工作原理是:线程在用户态循环尝试CAS加锁,而不是立即进入内核态阻塞。自旋次数由JVM参数-XX:PreBlockSpin
控制(默认10次)。
自旋的优缺点:
优势 | 劣势 |
---|---|
避免线程上下文切换开销 | 自旋占用CPU资源 |
锁释放后可立即获取 | 长时间自旋导致CPU浪费 |
适合短时间锁竞争 | 不适合长时间锁占用 |
七、轻量级锁的适用场景与性能优化
7.1 最佳实践场景
- 短时间同步块(执行时间<100纳秒)
- 高并发但锁竞争不激烈的场景
- 单核CPU或超线程环境(自旋不会浪费额外CPU)
7.2 性能优化参数
-XX:PreBlockSpin
:调整自旋次数(默认10,可根据CPU核数调整)-XX:UseSpinning
:开启自旋(JDK8默认开启)-XX:SpinPolicy
:自旋策略(如adaptive
自适应自旋)
7.3 避免场景
- 长时间同步块(如IO操作、数据库访问)
- 单核CPU且负载高的环境
- 已知存在重度锁竞争的场景
八、总结
轻量级锁通过CAS操作和自旋等待,在无竞争或轻度竞争场景下将锁的性能损耗降至最低。它的设计体现了JVM并发优化的核心思想:在软件层面用算法优化(CAS)替代硬件层面的线程调度(阻塞)。
理解轻量级锁的工作原理,有助于在实际开发中:
- 选择更合适的同步方式(如
ConcurrentHashMap
的分段锁设计) - 优化锁竞争场景(如减小同步块粒度)
- 理解JVM锁升级的底层逻辑,避免写出"锁膨胀"严重的代码
当遇到锁性能问题时,可通过jstack
查看线程状态,结合-XX:+PrintLockInfo
日志分析锁升级路径,定位轻量级锁失效的具体原因。通过合理使用轻量级锁,可以显著提升Java应用的并发性能。