JVMNotes

第 10 章:synchronized

zjc 于 2026-01-10 发布

这是《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());

注意:

  1. identity hash 与部分锁状态存在交互;
  2. 锁状态是 JVM 实现细节;
  3. 业务代码不应依赖 Mark Word 布局;
  4. 性能分析可借助 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();
    }
}

规则:

  1. 必须在持有锁时调用 wait / notify;
  2. wait 要放在 while 循环中;
  3. wait 会释放锁;
  4. sleep 不释放锁;
  5. 优先使用 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) {
            }
        }
    }
}

产生条件:

  1. 互斥;
  2. 持有并等待;
  3. 不可剥夺;
  4. 循环等待。

避免:

  1. 统一加锁顺序;
  2. 缩小临界区;
  3. 使用 tryLock 超时;
  4. 一次事务只拿必要资源;
  5. 拆分锁粒度;
  6. 用超时和熔断控制外部调用。

10.8 锁粒度

过粗:

synchronized (globalLock) {
    processAllTenants();
}

问题:串行化、吞吐低、长尾高。

过细:

synchronized (map.get(tenantId)) {
}

风险:

  1. 锁对象可能变化;
  2. 复合操作不原子;
  3. 锁对象可能为 null;
  4. 引入隐藏竞态。

更清晰的方式:

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 性能建议

  1. 临界区只做内存操作和局部计算;
  2. 不要在锁内调用 RPC、数据库、文件 IO;
  3. 避免在锁内创建大量对象;
  4. 复合状态使用不可变快照;
  5. 优先并发容器;
  6. 读多写少可评估 ReadWriteLock 或 StampedLock;
  7. 用压测验证锁竞争;
  8. 保证正确性后再优化。

本章小结

synchronized 提供互斥和内存可见性,锁状态和优化由 JVM 根据竞争情况处理。现代 JDK 中应优先使用清晰简单的 synchronized,必要时再用 Lock。生产排障靠线程 dump、锁竞争指标和调用链定位。

思考题

  1. 实例方法和静态方法的锁对象分别是什么?
  2. 为什么 wait 必须写在 while 中?
  3. 死锁的四个条件是什么?
  4. 偏向锁的版本变化说明了什么?
  5. 如何定位 BLOCKED 线程的阻塞源头?