自旋锁本质是线程在用户态死循环检查锁是否释放,以CPU时间换调度开销;适用于临界区极短的多核场景,不适用I/O或复杂操作;JDK 6+默认启用适应性自旋,动态调整自旋次数。

自旋锁到底在“空转”什么?
自旋锁不是真的让线程“睡觉”,而是让它在用户态死循环检查锁是否被释放——不进内核、不挂起、不切换上下文。本质是用 CPU 时间换线程调度开销。
常见错误现象:Thread.getState() 返回 RUNNABLE,但实际啥也没干,CPU 使用率却莫名飙升;GC 日志里没看到大量线程阻塞,但吞吐量上不去。
- 只在多核机器上有效:单核 CPU 自旋 = 白白饿死其他线程
- 适用场景:临界区极短(比如几十纳秒到几微秒),例如计数器递增、简单状态标记更新
- 不适用场景:synchronized 块里有 I/O、sleep、数据库查询、复杂计算——这时自旋纯属浪费
- JDK 6+ 默认开启,无需加
-XX:+UseSpinning;但默认最多自旋10次,超时即走传统阻塞流程
为什么需要“适应性”?固定 10 次太傻了
固定次数的自旋就像定闹钟:锁刚释放你就停了,或者锁早该放了你还硬等满 10 轮——这两种都亏。
适应性自旋让 JVM 记住每个锁的历史表现:上次在这把锁上自旋成功了?持有锁的线程还在跑?那这次就多等几轮;如果连续几次都失败,下次干脆跳过自旋,直接挂起。
- 判断依据只有两个:
前一次自旋是否成功+当前持有锁的线程是否仍在运行中(not in BLOCKED/WAITING) - 它不依赖任何 JVM 启动参数,完全由 HotSpot 运行时动态决策
- 你无法用
-XX:PreBlockSpin干预适应性行为——这个参数只影响“非适应性”模式下的初始值,JDK 6+ 已基本弃用 - 注意:适应性只对同一对象实例上的锁生效;换一个对象,历史清零
怎么确认你的锁真在自旋?别靠猜
光看代码没法判断 synchronized 是否触发了自旋,得看运行时行为。最直接的方式是观察线程栈和锁竞争指标。
- 用
jstack <pid>查看线程状态:如果看到大量线程卡在java.lang.Thread.State: RUNNABLE,且堆栈停留在 synchronized 方法入口(如at java.util.HashMap.put(HashMap.java:...)),同时 CPU 高,大概率正在自旋 - 启用 JVM 锁统计:
-XX:+PrintGCDetails -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1,配合jstat -printcompilation <pid>观察 safepoint 停顿是否异常频繁(自旋失败后进入阻塞会触发 safepoint) - 不要依赖 VisualVM 或 JMC 的“线程 CPU 时间”排序——自旋线程显示为高 CPU,但未必是瓶颈;重点看它是否集中在某几个热点对象上
关掉自旋锁能解决问题吗?
不能。禁用自旋(-XX:-UseSpinning)只会让所有锁竞争立刻走重量级阻塞路径,反而可能放大性能问题。
- 副作用明显:线程频繁进出内核态 → 上下文切换暴涨 → 缓存失效加剧 → 实际延迟可能比自旋还高
- 真正该做的,是减少锁竞争本身:比如把大锁拆成小锁(分段锁)、用
java.util.concurrent替代 synchronized、或改用无锁结构(AtomicInteger,ConcurrentHashMap) - 如果发现某类对象上自旋失败率长期 >80%,说明它根本不适合自旋——这时应优先考虑消除锁(
-XX:+EliminateLocks)或关闭偏向锁(-XX:-UseBiasedLocking)来加速轻量级锁路径
自旋锁不是开关,是权衡。它不解决锁设计问题,只缓解调度代价;真正要调的,永远是锁的粒度和持有时间,而不是自旋次数。








