自旋锁在临界区极短、锁竞争不激烈且CPU资源充裕时比阻塞锁更省,因其避免了线程阻塞/唤醒的上下文切换开销,但会持续占用CPU。

自旋锁在什么情况下比阻塞更省?
不是所有场景自旋都更快。只有当临界区极短(通常 Unsafe.park() 阻塞调用本身就有可观开销,如果只是想等一个 volatile 变量翻转,自旋几轮确实划算。
常见错误现象:while (!locked.compareAndSet(false, true)) {} 写成死循环,没加 yield 或 pause,吃满单核;或者在高争用下还硬自旋,导致大量 CPU 空转、L1 缓存失效加剧。
- 适合场景:无锁队列的 head/tail 更新、CAS-based lock-free 数据结构内部重试、读多写少且写操作极快的共享标记位
- 不适合场景:IO 等待、数据库连接获取、任何涉及系统调用或不确定耗时的操作
- 多核优势只在「持有锁线程大概率仍在运行」时成立——若它刚被调度出去或正在 sleep,自旋纯属浪费
Java 中怎么控制自旋次数?没有统一 API
JVM 不暴露自旋次数配置给用户代码。HotSpot 对内置锁(synchronized)的自旋策略是动态的:早期版本靠 -XX:PreBlockSpin(已废弃),现在完全由 JVM 自适应决定,基于历史竞争模式预测是否值得自旋、自旋多久。你无法在应用层干预 synchronized 的自旋行为。
如果你用的是 java.util.concurrent 里的显式锁,比如 ReentrantLock,它的公平性开关(new ReentrantLock(true))会直接禁用自旋;而非公平模式下,lock() 内部的 nonfairTryAcquire() 会做一次 CAS 尝试,失败后立即 park,不自旋——也就是说,JUC 锁默认也不自旋。
立即学习“Java免费学习笔记(深入)”;
- 真要可控自旋,得自己写:用
Thread.onSpinWait()(JDK9+)替代空循环,让 CPU 知道这是忙等,可触发硬件优化(如 x86 的 PAUSE 指令) - 手写自旋必须设上限,比如
for (int i = 0; i - 别依赖
Thread.yield(),它不保证让出 CPU,在 Linux 上常被忽略,效果不稳定
为什么多核 CPU 下自旋不一定加速?
核心误区:以为“多核=能并行自旋=一定快”。实际上,如果自旋线程和持有锁线程跑在同一个物理核的超线程(HT)上,自旋不仅抢不到资源,还会加剧缓存乒乓(cache bouncing)——两个逻辑核反复争抢同一 L1 cache line,性能反而暴跌。
更隐蔽的问题是内存顺序干扰:频繁的 CAS 自旋会持续触发 store buffer 刷写和 invalidation 流量,拖慢整个 socket 的内存子系统。实测中,4 核机器上 4 个线程争一把锁,自旋 10 次比阻塞慢 20% 是常态。
- 真正受益的场景是:锁持有者固定绑定在某个核(如通过
taskset或pthread_setaffinity),且自旋线程也在同 die(非跨 NUMA node) - 现代 JVM 在检测到高争用时,会快速退化为阻塞,这个决策比人预设的阈值更靠谱
-
Thread.onSpinWait()能缓解但不能消除 cache bouncing,它只是降低功耗和干扰,不改变竞争本质
LockSupport.park() 被唤醒后,还要再 check 一次状态
这是最常漏掉的一环。自旋失败后调用 LockSupport.park(),线程挂起;但被唤醒时,不代表锁就拿到了——可能是虚假唤醒(spurious wakeup),也可能是唤醒后锁又被别人抢走。所以 park 返回后,必须重新检查条件,不能直接进临界区。
典型错误写法:LockSupport.park(); // 醒来就认为能进了。正确做法永远是「park 前检查 → park → 醒来再检查」的循环结构。
- 所有基于
LockSupport实现的锁(包括 AQS 子类)都遵循这个模式,AbstractQueuedSynchronizer的acquireQueued()里就有完整样板 - 即使用了自旋 + park 组合,也要把自旋逻辑包在 park 外层循环里,而不是自旋完直接进业务逻辑
- 这个二次检查的成本几乎为零(一次 volatile 读),但跳过它会导致严重并发 bug,且极难复现


















