这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。
synchronized 是 Java 内建的监视器锁,既有互斥功能,也有内存语义。HotSpot 对它做了偏向锁、轻量级锁、重量级锁等优化,不同 JDK 版本的实现和废弃策略也在变化。
10.1 基本用法
实例方法:
public synchronized void transfer() {
// 同一个实例互斥
}
静态方法:
public static synchronized void reload() {
// 类对象互斥
}
代码块:
private final Object lock = new Object();
public void update() {
synchronized (lock) {
// 指定对象互斥
}
}
三种锁对象不同:
| 写法 | 锁对象 |
|---|---|
| 实例方法 | 当前实例 |
| 静态方法 | 类 Class 对象 |
| 代码块 | 括号中的对象 |
不要用字符串常量、Integer 缓存对象等不可控对象做锁。
10.2 内存语义
解锁 happens-before 后续锁定:
Thread A
写共享变量
unlock
|
v happens-before
Thread B
lock
读共享变量
因此临界区内修改对后续进入同一锁的线程可见。
10.3 锁升级
传统 HotSpot 锁状态:
无锁
-> 偏向锁
-> 轻量级锁
-> 重量级锁
| 状态 | 适用 |
|---|---|
| 偏向锁 | 只有一个线程访问,JDK 15 后逐步废弃 |
| 轻量级锁 | 短暂交替竞争,CAS 膨胀 |
| 重量级锁 | 竞争强,依赖操作系统 mutex,可能阻塞 |
JDK 15 引入默认禁用偏向锁,后续版本逐步移除。不要把锁升级细节写成永久不变的结论,应以当前 HotSpot 文档为准。
10.4 对象头与 Mark Word
对象头 Mark Word 会存储锁状态、identity hash、GC 年龄等信息。不同状态下位含义不同。
查看:
System.out.println(ClassLayout.parseInstance(object).toPrintable());
注意:
- identity hash 与部分锁状态存在交互;
- 锁状态是 JVM 实现细节;
- 业务代码不应依赖 Mark Word 布局;
- 性能分析可借助 JOL 和日志。
10.5 锁优化
JIT 可能执行:
| 优化 | 场景 |
|---|---|
| 锁消除 | 对象不可能逃逸被其他线程访问 |
| 锁粗化 | 相邻重复锁同一对象 |
| 自旋 | 短暂等待,避免立即阻塞 |
逃逸分析示例:
public String build() {
StringBuffer buffer = new StringBuffer();
buffer.append("a");
return buffer.toString();
}
如果 buffer 不逃逸,相关同步可能被消除。这不代表业务应滥用同步,而是说明 JVM 能优化明显局部对象。
10.6 等待通知
private final Object lock = new Object();
private boolean ready = false;
public void consume() throws InterruptedException {
synchronized (lock) {
while (!ready) {
lock.wait();
}
// 消费
}
}
public void produce() {
synchronized (lock) {
ready = true;
lock.notifyAll();
}
}
规则:
- 必须在持有锁时调用 wait / notify;
- wait 要放在 while 循环中;
- wait 会释放锁;
- sleep 不释放锁;
- 优先使用 Condition 或高层队列。
10.7 死锁
public class Deadlock {
private final Object a = new Object();
private final Object b = new Object();
public void left() {
synchronized (a) {
synchronized (b) {
}
}
}
public void right() {
synchronized (b) {
synchronized (a) {
}
}
}
}
产生条件:
- 互斥;
- 持有并等待;
- 不可剥夺;
- 循环等待。
避免:
- 统一加锁顺序;
- 缩小临界区;
- 使用 tryLock 超时;
- 一次事务只拿必要资源;
- 拆分锁粒度;
- 用超时和熔断控制外部调用。
10.8 锁粒度
过粗:
synchronized (globalLock) {
processAllTenants();
}
问题:串行化、吞吐低、长尾高。
过细:
synchronized (map.get(tenantId)) {
}
风险:
- 锁对象可能变化;
- 复合操作不原子;
- 锁对象可能为 null;
- 引入隐藏竞态。
更清晰的方式:
private final ConcurrentHashMap<String, Object> locks = new ConcurrentHashMap<>();
Object lock(String tenantId) {
return locks.computeIfAbsent(tenantId, k -> new Object());
}
如果锁对象会累积,还需要清理策略。
10.9 synchronized 与 Lock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 语法 | 语言内建 | API |
| 释放 | 自动 | finally 显式 unlock |
| tryLock | 不支持 | 支持 |
| 超时 | 不支持 | 支持 |
| 公平锁 | 不支持 | 可选 |
| Condition | 单一 wait 队列 | 多 Condition |
| 可中断 | 不支持 | lockInterruptibly |
优先 synchronized;只有需要超时、可中断、公平或多条件队列时再考虑 Lock。
10.10 生产排查
线程 dump:
jstack <pid>
jcmd <pid> Thread.print -l
arthas thread -b
死锁输出:
Found one Java-level deadlock
常见阻塞状态:
| 状态 | 含义 |
|---|---|
| BLOCKED | 等待监视器锁 |
| WAITING | wait / join / park |
| TIMED_WAITING | 有超时等待 |
| RUNNABLE | 运行或等待 IO |
锁监控:
jstat -printsynchro <pid>
jcmd <pid> Thread.print
10.11 性能建议
- 临界区只做内存操作和局部计算;
- 不要在锁内调用 RPC、数据库、文件 IO;
- 避免在锁内创建大量对象;
- 复合状态使用不可变快照;
- 优先并发容器;
- 读多写少可评估 ReadWriteLock 或 StampedLock;
- 用压测验证锁竞争;
- 保证正确性后再优化。
本章小结
synchronized 提供互斥和内存可见性,锁状态和优化由 JVM 根据竞争情况处理。现代 JDK 中应优先使用清晰简单的 synchronized,必要时再用 Lock。生产排障靠线程 dump、锁竞争指标和调用链定位。
思考题
- 实例方法和静态方法的锁对象分别是什么?
- 为什么 wait 必须写在 while 中?
- 死锁的四个条件是什么?
- 偏向锁的版本变化说明了什么?
- 如何定位 BLOCKED 线程的阻塞源头?