锁优化
Java 中 synchronized 的性能已经经过长期优化。现代 JVM 会根据运行时情况对锁进行多种优化,减少无竞争或低竞争场景下的开销。
# 1. synchronized 的基本语义
synchronized 保证:
- 互斥。
- 可见性。
- 有序性。
synchronized (lock) {
// critical section
}
进入同步块需要获取监视器锁,退出同步块释放锁。
# 2. 锁状态
HotSpot 对对象锁做了多种状态优化。
常见锁状态可以理解为:
- 无锁。
- 偏向锁。
- 轻量级锁。
- 重量级锁。
需要注意:偏向锁在 JDK 15 中已经默认禁用,并废弃了相关命令行参数,学习时理解历史演进即可。现代实践中更关注轻量级锁、重量级锁和锁消除等优化。
# 3. 轻量级锁
轻量级锁适合线程交替进入同步块、竞争不激烈的场景。
它尝试通过 CAS 避免直接进入操作系统互斥量,减少内核态切换开销。
如果竞争加剧,轻量级锁可能膨胀为重量级锁。
# 4. 重量级锁
重量级锁会涉及操作系统互斥量,竞争失败的线程可能被阻塞和唤醒。
这会带来:
- 上下文切换。
- 内核态开销。
- 调度延迟。
高竞争同步块可能严重影响性能。
# 5. 锁消除
如果逃逸分析发现某个锁对象不会被其他线程访问,JIT 可以消除这个锁。
public void method() {
Object lock = new Object();
synchronized (lock) {
// no escape
}
}
这种锁没有实际同步意义。
# 6. 锁粗化
如果 JVM 发现一系列连续操作反复加锁解锁,可能把多个锁操作合并成一个更大范围的锁。
for (int i = 0; i < 100; i++) {
synchronized (lock) {
doSomething(i);
}
}
锁粗化可以减少频繁加锁解锁开销,但也可能扩大锁持有时间。JVM 会根据优化策略决定。
# 7. 工程实践
写并发代码时,不要过度依赖 JVM 自动优化。更重要的是:
- 缩小临界区。
- 避免锁内执行 IO。
- 避免锁内调用外部服务。
- 减少热点锁竞争。
- 合理使用并发容器。
- 必要时拆分锁或使用无锁结构。
# 8. 阶段小结
JVM 对锁做了很多优化,使 synchronized 在无竞争和低竞争下性能较好。但高竞争锁仍然会成为瓶颈。锁优化知识帮助我们理解 JVM 行为,工程上仍要从设计上减少共享和竞争。
# 9. 锁状态变化图
无锁
│
├─ 低竞争
▼
轻量级锁
│
├─ CAS 失败、竞争加剧
▼
重量级锁
│
└─ 线程阻塞和唤醒涉及操作系统
偏向锁是历史上的低竞争优化,JDK 15 起默认禁用并废弃相关参数。学习锁状态时要结合 JDK 版本。
# 10. 锁竞争排查
锁竞争可能表现为:
- CPU 高。
- 响应时间长。
- 大量线程
BLOCKED。 - JFR 中 Monitor Blocked 事件多。
- 线程栈集中在某个 synchronized 块。
排查工具:
jstack看 BLOCKED 线程。- JFR 看锁竞争事件。
- async-profiler 看锁 profiling。
- 应用指标看队列和线程池。
# 11. 工程优化方向
| 问题 | 优化方向 |
|---|---|
| 锁粒度太大 | 缩小临界区 |
| 锁内 IO | 把 IO 移出锁 |
| 单锁热点 | 分段锁、拆分数据 |
| 读多写少 | ReadWriteLock、CopyOnWrite、不可变对象 |
| 计数热点 | LongAdder |
| 共享状态复杂 | 消息队列、Actor、串行化处理 |
# 12. Tips 快问快答
Q:synchronized 一定慢吗?
A:不是。现代 JVM 对无竞争和低竞争场景优化很多,真正慢的是高竞争和锁内慢操作。
Q:锁消除由谁做?
A:JIT 基于逃逸分析判断锁对象是否可能被其他线程访问。
Q:锁粗化一定好吗?
A:不一定。它减少加解锁次数,但可能扩大锁持有时间。
Q:怎么判断锁竞争严重?
A:看线程栈 BLOCKED 数量、JFR 锁事件、RT 抖动和 CPU 使用情况。