自旋锁通过用户态空转避免上下文切换开销,仅适用于临界区极短(如原子操作)的场景;jdk 6+ synchronized 默认启用自适应自旋,reentrantlock 依赖aqs有限自旋;滥用会导致cpu飙升,关键在适时放弃并升级为重量级锁。

自旋锁通过让线程在用户态“原地空转”,反复检查锁是否释放,从而跳过线程挂起和唤醒的全过程,直接避免了从用户态到内核态再切回的上下文切换——一次切换通常消耗 1000+ 时钟周期,而自旋只是几条轻量汇编指令的循环。
适用场景要足够短
自旋只在临界区执行时间极短时才真正有效,比如字段赋值、原子计数器增减等纳秒级操作。一旦里面出现 I/O、sleep、wait 或较重计算,自旋就变成纯耗 CPU 的无效等待,反而拖慢整体吞吐。
- 典型安全用法:synchronized 块内仅做 int++、boolean 标志位修改
- 明显风险操作:调用了 Thread.sleep()、Object.wait()、数据库查询、网络请求
- 可观察指标:jstack 显示大量线程卡在 Unsafe.park 前的 CAS 循环中,top 看 CPU 高但 QPS 上不去
JVM 默认已启用自适应自旋
从 JDK 6 起,synchronized 内部的轻量级锁路径默认开启自适应自旋,不需要手动配置。它不是固定旋 10 次,而是根据历史行为动态调整:如果上次在同一把锁上自旋成功,且持有锁的线程仍在运行,这次就会多给几次机会;反之则可能直接阻塞。
Java JDK 25 来自 OpenJDK 官方归档,版本为 JDK 25,本条下载地址已指向官方 Windows x64 zip 安装包直链,适合调试旧项目或兼容旧版 Java 运行环境。
- 该机制由 HotSpot 的 ObjectSynchronizer::fast_enter 等路径触发,开发者无法干预逻辑
- -XX:PreBlockSpin=20 可设上限,但不推荐——自适应本身比硬编码更贴合实际负载
- 注意:自适应只对同一锁对象生效,不同对象之间历史互不影响
ReentrantLock 需要手动实现或依赖AQS
不同于 synchronized,ReentrantLock 本身不内置自旋逻辑,但其底层 AQS(AbstractQueuedSynchronizer)在 tryAcquire 失败后,默认会先尝试有限自旋(尤其在多核环境下),再进入阻塞队列。你可以通过自定义 Sync 子类或结合 LockSupport.parkNanos() 控制等待节奏。
- 简单自旋锁可用 AtomicReference + compareAndSet 实现,但务必加退出条件或 yield() 降低总线争用
- 高并发中间件如 RocketMQ 曾因纯自旋导致 CPU 暴涨,后来改用 ABS(Adaptive Backoff Spin)策略,在自旋失败后逐步延长等待间隔
- 不建议在业务代码中手写底层自旋锁,优先用 JVM 已优化好的 synchronized 或带公平性控制的 ReentrantLock
自旋不是万能,关键在“适时放弃”
真正的优化点不在“要不要自旋”,而在于“什么时候该停”。单核 CPU 上多线程争一把锁时,自旋不仅抢不到锁,还会饿死持有锁的线程;锁被长时间占用时,自旋只会浪费算力。JVM 的轻量级锁机制正是靠“CAS 尝试 + 自旋阈值 + 升级为重量级锁”这一组合,把挂起推迟到真正必要时才发生。
- 锁升级路径:无锁 → 偏向锁 → 轻量级锁(含自旋)→ 重量级锁
- 自旋失败后升级为重量级锁,意味着开始依赖操作系统互斥量,线程真正挂起
- 排查时重点看对象头 Mark Word 的锁状态位变化,以及线程堆栈中是否频繁出现 park/unpark 调用链
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










