为什么JDK15要废弃偏向锁?
核心结论
JDK 15 默认禁用偏向锁(-XX:-UseBiasedLocking),JDK 18 完全移除。主要原因是:偏向锁的设计已不适应现代 Java 应用的并发模式,维护成本高,收益有限。
官方说明
JEP 374: Deprecate and Disable Biased Locking
Summary: Disable biased locking by default, and deprecate all related command-line options.
Motivation: The performance gains of biased locking are no longer as significant as they once were, and the complexity of the code makes it difficult to maintain.
关键点:
- 性能收益不再显著
- 代码复杂度高,维护困难
- 现代应用的并发模式已改变
废弃原因详解
1. 现代应用的并发特点变化
偏向锁的设计初衷(2004年 JDK 5)
// 早期 Java 应用的典型场景
public class LegacyApp {
public synchronized void process() {
// 单线程反复获取同一把锁
// 例如:Swing GUI 事件处理
}
}
设计假设:
- 大多数锁没有竞争
- 同一线程会反复获取同一把锁
- 单线程应用居多
现代 Java 应用的特点(2020+)
// 现代 Java 应用的典型场景
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // 每次请求不同线程处理
}
}
现实情况:
- 线程池广泛使用:Tomcat、Netty、Spring WebFlux
- 请求处理模型:不同线程处理不同请求
- 对象复用:连接池、对象池中的对象被多个线程访问
- 高并发场景:微服务、分布式系统
结果:偏向锁频繁撤销,反而成为性能瓶颈!
2. 偏向锁的性能开销
偏向锁撤销的代价
// 典型场景:线程池 + 对象池
ExecutorService executor = Executors.newFixedThreadPool(10);
BlockingQueue<Connection> pool = new LinkedBlockingQueue<>();
// 连接对象内部有 synchronized 方法
Connection conn = pool.take();
executor.submit(() -> {
synchronized(conn) { // 偏向撤销!(不同线程访问)
conn.execute(sql);
}
});
开销分析:
| 操作 | 时间开销 |
|---|---|
| 偏向锁加锁(重入) | ~5 ns |
| 轻量级锁 CAS | ~20 ns |
| 偏向锁撤销(STW) | ~1-10 μs |
| 批量重偏向 | ~100 μs |
问题:
- 偏向撤销需要 Stop-The-World(暂停所有线程)
- 在高并发场景下,频繁 STW 严重影响吞吐量
- 批量重偏向的触发条件难以预测
实际性能测试
// 测试代码
public class BiasedLockBenchmark {
private final Object lock = new Object();
@Benchmark
public void singleThread() {
synchronized(lock) {
// 偏向锁:5 ns
}
}
@Benchmark
@Threads(10)
public void multiThread() {
synchronized(lock) {
// 偏向撤销 + 轻量级锁:50-100 ns
// 比直接使用轻量级锁更慢!
}
}
}
结论:在多线程场景下,偏向锁的撤销开销使其性能不如直接使用轻量级锁。
3. 维护成本高
代码复杂度
偏向锁的实现涉及大量复杂逻辑:
// HotSpot 源码中的偏向锁相关代码
biasedLocking.cpp // 核心逻辑(2000+ 行)
biasedLocking.hpp // 头文件
synchronizer.cpp // 锁升级逻辑
markOop.hpp // Mark Word 操作
// 关键机制
- Epoch 版本管理
- 批量重偏向
- 批量撤销
- STW 协调
- 多线程同步
维护负担
- Bug 修复困难:
- 偏向锁相关 Bug 难以复现(时序相关)
- 涉及 GC、JIT 编译器等多个子系统交互
- 性能回归风险:
- 每次修改需要大量测试
- 不同场景下的行为差异大
- 架构演进受阻:
- ZGC、Shenandoah 等新 GC 需要额外适配偏向锁
- 限制了 JVM 的优化空间
4. 轻量级锁的优化已足够
自适应自旋的改进
JDK 6 之后的轻量级锁已经非常高效:
// 轻量级锁的性能表现
synchronized(lock) {
// CAS 操作:~20 ns(无竞争)
// 自适应自旋:根据历史数据调整
// 成功率高的场景:接近偏向锁性能
}
性能对比(现代硬件)
| 场景 | 偏向锁 | 轻量级锁 | 优势方 |
|---|---|---|---|
| 单线程反复获取 | 5 ns | 20 ns | 偏向锁 |
| 两线程交替访问 | 50 ns | 30 ns | 轻量级锁 |
| 多线程低竞争 | 100 ns | 40 ns | 轻量级锁 |
| 线程池场景 | 200 ns | 50 ns | 轻量级锁 |
关键点:
- 现代 CPU 的 CAS 性能提升(缓存一致性协议优化)
- 轻量级锁的自适应自旋更智能
- 避免了 STW 的全局影响
5. 实际应用场景分析
场景1:Tomcat 请求处理
// Tomcat 线程池处理 HTTP 请求
public class Servlet {
private final DataSource ds = ...; // 连接池
protected void doGet(HttpServletRequest req, ...) {
Connection conn = ds.getConnection(); // 不同线程获取同一连接
synchronized(conn) { // ❌ 偏向锁频繁撤销
// ...
}
}
}
问题:连接池对象被多个线程访问,偏向锁无法生效。
场景2:Spring Bean
@Service
public class UserService {
@Autowired
private UserRepository repo; // 单例 Bean
public synchronized void updateUser(User user) {
// 多个线程调用同一个 Bean
// 偏向锁撤销 → 轻量级锁
}
}
问题:单例 Bean 被多个请求线程访问,偏向锁退化。
场景3:真正受益的场景(罕见)
// 单线程 GUI 应用(已不常见)
public class SwingApp {
private final Object lock = new Object();
// EDT 线程反复调用
public synchronized void paintComponent(Graphics g) {
// 偏向锁有效 ✓
}
}
现实:这类应用在现代 Java 生态中占比极小。
迁移策略
如何应对偏向锁废弃?
1. 大多数应用:无需改动
// 原有代码无需修改
public synchronized void method() {
// JVM 自动使用轻量级锁
// 性能可能更好
}
2. 性能敏感场景:评估和优化
# 测试禁用偏向锁的影响
java -XX:-UseBiasedLocking -jar app.jar
# 对比性能
jmh-benchmark.jar
3. 如果确实需要偏向锁(JDK 15-17)
# 手动开启(不推荐)
java -XX:+UseBiasedLocking -jar app.jar
# 监控偏向锁统计
-XX:+PrintBiasedLockingStatistics
4. 长期方案:优化锁设计
// ❌ 避免不必要的 synchronized
public synchronized void getAllUsers() {
return new ArrayList<>(users); // 防御性拷贝不需要锁
}
// ✅ 使用并发集合
private final ConcurrentHashMap<Long, User> users = ...;
public User getUser(Long id) {
return users.get(id); // 无锁读取
}
其他 JVM 的实践
| JVM | 偏向锁支持 | 说明 |
|---|---|---|
| HotSpot JDK 15+ | 默认禁用 | JEP 374 |
| OpenJ9 | 从未实现 | 认为收益有限 |
| GraalVM | 不支持 | 专注于 AOT 编译优化 |
| Azul Zing | 已废弃 | 使用更高效的锁算法 |
趋势:主流 JVM 都在放弃偏向锁。
总结对比
偏向锁的历史使命
2004年(JDK 5):
- 单线程应用为主
- GUI、Applet 流行
- 锁竞争少
→ 偏向锁效果显著
2020年(JDK 15):
- 微服务、云原生架构
- 线程池、对象池普及
- 高并发成为常态
→ 偏向锁已过时
废弃原因总结
- 现代应用的并发模式改变:线程池 + 对象池 → 偏向锁频繁撤销
- 性能收益有限:轻量级锁 + 自适应自旋已足够快
- 维护成本高:代码复杂、Bug 多、阻碍 JVM 演进
- STW 影响大:偏向撤销的全局暂停影响吞吐量
- 硬件进步:现代 CPU 的 CAS 性能大幅提升
面试答题模板
问题:为什么 JDK 15 要废弃偏向锁?
回答:
JDK 15 通过 JEP 374 默认禁用偏向锁,主要有以下几个原因:
- 现代应用并发模式改变:
- 早期偏向锁设计假设单线程反复获取锁(如 Swing 应用)
- 现代应用大量使用线程池、对象池,导致偏向锁频繁撤销
- 例如 Tomcat 线程池处理请求,每个连接被不同线程访问
- 性能收益降低:
- 偏向撤销需要 Stop-The-World,开销 1-10 微秒
- 频繁撤销导致性能不如直接用轻量级锁
- 现代 CPU 的 CAS 性能大幅提升,轻量级锁已足够快
- 维护成本高:
- 偏向锁代码复杂(Epoch、批量重偏向、STW 协调)
- 相关 Bug 难以修复,阻碍 JVM 演进
- 新 GC(ZGC、Shenandoah)需要额外适配
- 替代方案成熟:
- 轻量级锁的自适应自旋已经很高效
- 并发工具类(ConcurrentHashMap、LockSupport)更适合现代场景
因此,JDK 15 废弃偏向锁,直接从无锁升级到轻量级锁,简化了 JVM 实现,同时在大多数场景下性能反而更好。