先看两个数。
JDK 11,开着偏向锁,一个单线程里反复进入 synchronized 方法、给计数器加一,平均 0.29 纳秒一次。JDK 21,同样的代码,5.42 纳秒。
快了二十年、慢回去十八倍。中间发生的事,是 HotSpot 把偏向锁整个删掉了。
发生了什么:JEP 374 与一次没有 JEP 的删除
时间线不长,但容易记混。
| 版本 | 发生了什么 | 依据 |
|---|---|---|
| Java SE 6 | 偏向锁默认开启 | Oracle Java SE 6 性能白皮书 |
| JDK 15 | 默认关闭,相关参数标记废弃 | JEP 374 |
| JDK 18 | 代码删除,参数失效 | JDK-8256425 |
| JDK 19 | 参数计划彻底过期 | hotspot-dev 讨论 |
| JDK 21 | -XX:+UseBiasedLocking 被当成未知参数拒绝 |
实测 |
注意第三行和第四行:删除本身没有单独的 JEP。JEP 374 只做到「废弃并禁用」,真正把代码拿掉是一个普通变更单,编号 JDK-8256425。所以你在网上会看到「偏向锁是 JDK 15 删的」和「JDK 18 删的」两种说法,都不算错,说的是两件事。
作者在一台机器上装了 JDK 11、17、21、25,跑两个微基准:一个在 synchronized 方法里自增,一个往 StringBuffer 里 append(StringBuffer 的每个方法都是同步的)。JDK 11 和 17 上分别开关偏向锁,21 和 25 没得选。
| 环境 | 计数器(纳秒/次) | StringBuffer(纳秒/次) |
|---|---|---|
| JDK 11,偏向锁开(默认) | 0.29 | 1.7 |
| JDK 11,偏向锁关 | 5.5 | 22.2 |
| JDK 17,偏向锁开(需手动加参数) | 0.24 | 1.9 |
| JDK 17,默认(关) | 5.4 | 21.9 |
| JDK 21 | 5.4 | 22.0 |
| JDK 25 | 5.4 | 21.4 |
StringBuffer 那一列更刺眼。偏向锁开着的时候,每个方法都同步的 StringBuffer,和完全不同步的 StringBuilder(JDK 25 上 1.65 纳秒)几乎一样快。关掉之后,同一段代码慢十三倍。
机制:偏向锁到底在省什么,删掉又省了什么
要理解这件事,得先知道 Java 的一个对象在内存里长什么样。
每个对象头部有一块叫 Mark Word 的区域,存哈希码、GC 分代年龄、锁状态。HotSpot 用最低两位标记锁状态:未锁定、轻量级锁、重量级锁、GC 标记。所谓加锁,第一步是把 Mark Word 换成一个指向锁记录的指针,这一步用 CAS(比较并交换)完成,是一条原子指令。
原子指令在 2006 年很贵。Dave Dice 当年那份 Java SE 6 同步机制的幻灯片里,把原子 CAS 的高延迟列为无竞争锁的主要成本。问题是绝大多数锁根本没人抢:一个线程锁住一个对象,用完放开,再锁,循环几百万次,从头到尾没有第二个线程出现。每次都付一次原子指令的钱,纯属浪费。
那个年代的硬件背景得说清楚。多核处理器刚普及,缓存一致性协议在核数上去之后开销陡增,一条 lock 前缀指令可能需要几十个时钟周期。同一时期 Java 企业应用里满地都是 Hashtable 和 Vector,每个 get、put 都同步一次,而这些对象在绝大多数代码里只有一个线程在用。偏向锁就是冲着这个场景去的,投入产出比在当时极高。
偏向锁的思路是让锁记住主人。第一次加锁时,JVM 把当前线程的 ID 写进 Mark Word,把偏向位置 1。之后同一个线程再进来,只要比较一下线程 ID 就放行,那条 CAS 直接跳过。JVM 在赌「这个线程会回来」。
赌输的代价不小。第二个线程来抢,JVM 必须撤销偏向,而撤销需要在安全点(safepoint)上做,要暂停线程、检查栈帧里还有没有这个对象的锁记录。JEP 374 的原话是撤销「昂贵」。Oracle 从 JRockit 迁到 HotSpot 的指南里写得更直白:竞争激烈的应用把偏向锁关掉可能更快。
那为什么还要删?JDK-8231265 给了三条理由。一是原子指令在新处理器上便宜了;二是真正受益的是 Hashtable、Vector 这类每个方法都同步的老类,新代码不怎么用;三是偏向锁的代码复杂度高,散落在 HotSpot 好几个模块里,维护成本压不住。
第三条值得多说一句。偏向锁不是一个独立的开关,它和对象头布局、栈上锁记录、安全点机制、JIT 的锁消除与锁粗化都缠在一起。删掉它,等于给整个运行时减一层特判。
要看清它在整个体系里的位置,得把四种锁状态排一遍。
无锁时,Mark Word 存哈希码和分代年龄。偏向锁把线程 ID 写进去,第二次进入只做比较。轻量级锁换了个做法:把 Mark Word 复制到当前线程栈上的 Lock Record,再用 CAS 让对象头指向这个 Lock Record;如果 CAS 失败,说明有人抢,升级。重量级锁把请求交给 ObjectMonitor,线程进等待队列,由操作系统调度挂起。
偏向锁省掉的是「第一次之后的所有 CAS」,轻量级锁省掉的是「进内核」,重量级锁省掉的是「自旋等待」。三者解决的是不同层面的问题,所以删掉偏向锁并不会让轻量级锁也失效,只是把「同一线程反复进入」这个最常见场景重新打回每次一次 CAS。
当年这套设计还有一层补丁,叫批量重偏向(bulk rebias)。单个对象撤销偏向很贵,但如果一整类对象的偏向都失效了(典型情况是同一批对象换了使用线程),JVM 会用一个 epoch 值把这一批的偏向一次性作废再重新偏,避免逐个撤销。Russell 和 Detlefs 在 2006 年的 OOPSLA 论文里讲的就是这个。这套机制跟着偏向锁一起被删了,现在也没人需要它。
对照:被删的不只是「一个优化」
先看一个反例,说明这个优化的实际份量。
JDK-8329619 是一张与删除相关的缺陷单,里面记录了一个 Spark 压缩相关的微基准在失去偏向锁后大约四倍的回归。四倍听起来吓人,但要看基数:那是微基准,测的是纯锁路径,真实业务里锁的占比通常远低于这个比例。
再看尺度。5 纳秒一次无竞争锁,一百万次是 5 毫秒。如果你的服务每秒处理一万次请求、每次请求进出十几把锁,一天下来多出来的开销也就几百毫秒量级。真正会疼的是循环里高频加锁的老代码,比如用 StringBuffer 在循环里拼字符串。
同一个时期 JVM 还做了别的事,方向是反的:轻量级锁和重量级锁的分层、锁消除(逃逸分析证明对象不出线程就直接不加锁)、锁粗化(把相邻的同步块合并)。这些优化在偏向锁消失之后承担了大部分工作。JDK 21 之后的虚拟线程又带来一个新维度:在 JDK 21 到 23 上,synchronized 块里发生阻塞会把载体线程一起钉住(pinning),这个坑一直到 JDK 24 的 JEP 491 才填上。所以「synchronized 便宜了还是贵了」,答案取决于你问的是哪条路径。
横向比一下别的语言也很有意思。Go 的 sync.Mutex 快路径就是一次 CAS 加一次原子写,没有「记住主人」这种优化,设计上更接近偏向锁被删掉的 JVM。Rust 的 std::sync::Mutex 在 Linux 上落到 futex,无竞争时也是一次原子操作。0.3 纳秒那个数字是个特例,5.4 纳秒才是这类锁的常态成本。JVM 曾经比所有人便宜二十倍,现在回到了同一起跑线。
还有一层是编译器帮你藏起来的。C2 的锁消除会在逃逸分析证明对象不会跑出当前线程时,把整块同步直接删掉。所以有些代码你看着每个方法都 synchronized,实测却完全没有锁开销,因为 JIT 后发现根本不需要。这也解释了为什么很多人在 JDK 21 上跑自己的服务,感觉不到任何变化:热点循环里的锁早就被消掉了,剩下的都在冷路径上。
对谁有影响:三类代码,三种反应
第一类,单线程持有的同步集合。 这是最直接的受害者,也是最容易修的。StringBuffer 换成 StringBuilder,Vector 换成 ArrayList,Hashtable 换成 HashMap;如果确实有并发,用 ConcurrentHashMap 而不是把 Hashtable 包一层 Collections.synchronizedMap。JEP 374 自己就写了:依赖不必要同步的应用,换到不同步的类之后大概率会变快。
第二类,从 JDK 11 或 17 升级上来的服务。 升级前先搜一遍这三个类名。很多项目的依赖树里还挂着老库,它们内部用 Vector 或 StringBuffer,你自己代码干净不代表调用栈干净。
第三类,写基准测试的人。 这条最容易被忽略。如果你在 JDK 11 时代测过一个同步路径的吞吐,拿那个数当基线,升到 21 之后会看到莫名其妙的退化,然后开始怀疑 JIT、怀疑 GC、怀疑容器 CPU 限制。先查清楚当年那次测量是不是吃了偏向锁的红利。
顺带说一句启动脚本。任何还带着 -XX:+UseBiasedLocking 或 -XX:-UseBiasedLocking 的脚本都要清掉,JDK 21 会直接拒绝启动,报「Unrecognized VM option」。这个错误很显眼,反倒是好事;真正麻烦的是那些写在 Dockerfile、Helm chart、systemd unit 深处、平时没人看的参数。
如果你在用虚拟线程,还有一条和锁直接相关的取舍:JDK 21 到 23 上,synchronized 块里一旦阻塞就会把载体线程钉住,吞吐掉得比预期快。这期间能换 ReentrantLock 的地方就换,JDK 24(JEP 491)之后不必再迁就。
边界:这组数字能推到多远
作者自己交代了测量条件:没有用 JMH,单核沙盒,两次运行、每次九轮取中位数。这种测法能看出量级,不能当精确基准。0.29 和 5.42 之间的二十倍差距,方向可信;具体到 5.42 还是 5.1,没意义。
还有三点会让结论走样。
一是只测了无竞争路径。偏向锁在真有竞争的场景下本来就是负资产,撤销成本会吃掉收益。如果你的锁大部分时间都在抢,删掉偏向锁对你可能是好事。
二是原子指令的成本跟 CPU 架构、核数、缓存一致性协议都有关。测试机是一台机器,换一台 EPYC 或者 Apple Silicon,绝对值会变。
三是 JIT 分层编译的介入时间。偏向锁在启动几秒后才激活(HotSpot wiki 有这条),而微基准如果跑在 C1 阶段,测到的可能根本不是 C2 优化后的代码。
最该补的一块是多线程场景。这次只测了单线程反复进入,没有测第二个线程出现时会发生什么。按 JEP 374 的说法,撤销偏向要在安全点上做,成本远高于一次 CAS,所以在有轻度竞争的场景里,偏向锁大概率是净亏。删掉它,对这类负载是改善。文章里那二十倍的差距只描述了天平的一端,另一端现在无从测量------代码已经没了。
接下来看什么
三件可以自己动手验证的事,不需要等谁下结论。
一,在你的代码库和依赖树里 grep StringBuffer、Vector、Hashtable,看有多少是单线程持有的。这个数通常比想象的多。
二,升级到 JDK 21 或 25 之后,用 JMH 把最关键的那条同步路径重测一遍,带上 -prof perfasm 看热点是不是落在锁指令上。
三,跑 java -XX:+PrintFlagsFinal -version | grep -i biased。如果什么都没有输出,说明这个开关在你的 JVM 上已经不存在了------这也是判断「网上的调优建议是不是过期」最快的一招。
来源:
- Java Code Geeks,《Why synchronized Got Fast: The Rise and Removal of Biased Locking》,2026-11-04,www.javacodegeeks.com/2026/11/why...
- OpenJDK,《JEP 374: Deprecate and Disable Biased Locking》,openjdk.org/jeps/374
- OpenJDK 缺陷单 JDK-8256425(JDK 18 移除偏向锁代码)、JDK-8231265(移除理由)、JDK-8329619(Spark 压缩基准回归)
- Oracle,《Java SE 6 Performance White Paper》,www.oracle.com/java/techno...
- Russell & Detlefs,《Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing》,OOPSLA 2006