并发编程

Java 锁机制:synchronized、Lock 与读写锁

建立 Java 锁的分类体系,理解 Monitor、ReentrantLock、读写锁、StampedLock 及其工程选型。

难度:深入更新:2026-07-16

一、锁分类不是互斥标签

悲观/乐观、独占/共享、公平/非公平、可重入/不可重入、自旋/阻塞描述的是不同维度。一把锁可以同时具备多个特征。

Java 锁机制分类与典型实现

例如 ReentrantLock 是悲观、独占、可重入锁,支持公平或非公平策略;ReentrantReadWriteLock.ReadLock 属于共享读锁;StampedLock 支持写锁、悲观读和乐观读,但不可重入。

二、synchronized 的使用方式

public synchronized void instanceMethod() {}

public static synchronized void staticMethod() {}

public void block() {
    synchronized (lock) {
        // 临界区
    }
}

对应锁对象:

  • 实例同步方法锁当前实例;
  • 静态同步方法锁对应 Class 对象;
  • 同步代码块锁括号中的对象。

同步代码块在字节码层面通常使用 monitorentermonitorexit;同步方法使用 ACC_SYNCHRONIZED 标志,由方法调用机制进入和退出 Monitor。

三、ObjectMonitor 的主要结构

每个 Java 对象都可关联 Monitor。竞争升级到需要阻塞管理时,可以用下图理解等待线程的组织方式:

synchronized 对应的 ObjectMonitor 结构

核心概念:

  • Owner:当前持有 Monitor 的线程;
  • EntryList:等待进入临界区的竞争线程;
  • WaitSet:调用 wait() 后释放 Monitor 并等待条件的线程;
  • notify() / notifyAll():使 WaitSet 中线程有资格重新参与锁竞争,不会让其立即执行。

wait 为什么必须在 synchronized 中使用

调用线程必须先拥有对象 Monitor,才能原子地释放锁并进入该 Monitor 的等待集合。否则会抛出 IllegalMonitorStateException

synchronized 的内存语义

对同一 Monitor 的解锁 happens-before 后续加锁。锁既提供临界区互斥,也建立共享数据的可见性和必要有序性。

四、synchronized 的历史锁优化

经典资料常描述:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

这反映了 HotSpot 历史实现中的优化思路:无竞争时尽量减少同步成本,短竞争时使用 CAS 和自旋,竞争加剧后使用 Monitor 阻塞管理。

需要注意版本差异:偏向锁在较新 JDK 中已被禁用并移除,不应把它当成所有现代 JDK 的当前执行流程。面试时应把“规范语义”与“某个 HotSpot 版本的优化实现”分开。

自旋

锁预计很快释放时,线程可以短暂自旋,避免挂起和唤醒;竞争时间过长则会浪费 CPU。现代 JVM 会根据运行情况决定部分自旋策略,业务代码不要自行模拟复杂的 JVM 锁升级逻辑。

锁消除

JIT 通过逃逸分析确认对象不会被其他线程访问时,可以消除不必要的同步。

锁粗化

连续对同一对象频繁加锁解锁时,JIT 可能扩大锁范围,减少重复同步成本。

五、ReentrantLock

ReentrantLock 基于 AQS 构建,提供与 synchronized 类似的可重入互斥语义,并增加显式控制能力。

标准写法:

lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

必须在 finally 中释放,避免异常导致锁永久不释放。

主要能力

  • lock():普通阻塞获取;
  • lockInterruptibly():等待时可以响应中断;
  • tryLock():立即尝试;
  • tryLock(timeout):超时获取;
  • 公平或非公平策略;
  • 一把锁可创建多个 Condition

公平与非公平

公平锁会优先考虑等待队列中更早的线程,减少插队和饥饿;非公平锁允许新线程先尝试抢锁,通常吞吐更高,因为刚运行的线程可能直接获得锁,减少调度切换。

公平不等于严格实时顺序,也不等于性能更高。

六、Condition

Condition 把一个锁拆出多个条件等待队列:

Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();

await() 会把当前线程加入条件队列并完全释放锁;signal() 只是把等待节点转移到 AQS 同步队列,线程仍需重新获取锁后才能从 await() 返回。

wait/notify 相比:

  • Object 通常只有一个 WaitSet;
  • ReentrantLock 可以创建多个 Condition;
  • Condition 能更精确地唤醒等待某类条件的线程。

七、ReentrantReadWriteLock

读写锁的基本规则:

读锁 + 读锁:可以并发
读锁 + 写锁:互斥
写锁 + 写锁:互斥

适合读多写少,并且读临界区足够长,值得承担锁管理成本的场景。读操作极短时,普通互斥锁或不可变快照可能更简单。

state 如何表示读写状态

经典实现中,AQS 的一个 int state 被拆成两部分:

高 16 位:共享读锁计数
低 16 位:独占写锁重入次数

每个线程自己的读锁重入次数还需要额外记录,不能只靠全局读计数判断。

锁降级

允许写线程在持有写锁时先获取读锁,再释放写锁:

写锁 → 同时持有读锁 → 释放写锁 → 保留读锁

这样可以在完成写入后继续安全读取更新结果。

不应直接从读锁升级为写锁。多个读线程同时升级会彼此等待,容易死锁。通常应释放读锁、获取写锁,并重新检查条件。

八、StampedLock

StampedLock 提供:

  • 独占写锁;
  • 悲观读锁;
  • 乐观读 stamp。

乐观读模式:

long stamp = lock.tryOptimisticRead();
double currentX = x;
double currentY = y;

if (!lock.validate(stamp)) {
    stamp = lock.readLock();
    try {
        currentX = x;
        currentY = y;
    } finally {
        lock.unlockRead(stamp);
    }
}

正确顺序是“先复制共享字段到局部变量,再校验”。校验成功后只使用局部快照;校验失败则加悲观读锁重读。

重要限制

  • 不可重入,同线程重复加同一锁可能自锁;
  • 不支持 Condition;
  • stamp 必须与锁模式匹配;
  • 乐观读不适合包含网络调用、数据库写入等不可逆副作用;
  • 写频繁时校验经常失败,收益可能消失。

如果数据可以设计为不可变对象并通过 volatile 引用整体替换,通常比 StampedLock 更简单。

九、synchronized 与 ReentrantLock

维度synchronizedReentrantLock
释放JVM 自动释放必须 finally 手动释放
可重入支持支持
公平策略不提供显式公平选项支持公平/非公平
中断等待不提供对应获取 APIlockInterruptibly()
超时尝试不支持tryLock(timeout)
条件队列每个对象一个 Monitor 等待集合一把锁可有多个 Condition
实现JVM Monitor 与运行时优化AQS

不要简单说 ReentrantLock 一定更快。现代 JVM 对 synchronized 做了大量优化,性能取决于竞争程度、临界区和功能需求。

十、锁如何选

优先 synchronized

  • 普通互斥;
  • 临界区结构简单;
  • 不需要超时、中断、公平和多个条件队列;
  • 希望异常时自动释放锁。

选择 ReentrantLock

  • 需要 tryLock、超时或中断;
  • 需要多个 Condition;
  • 需要显式公平策略;
  • 需要锁状态监控接口。

选择读写锁或 StampedLock

  • 读多写少,并已通过基准测试证明读并发收益明显;
  • StampedLock 仅用于低写冲突、短读取、能正确重试的场景。

优先考虑无共享设计

锁不是唯一方案。不可变对象、线程封闭、分片、消息队列串行化、并发容器和原子类往往能减少锁复杂度。

十一、常见锁问题

死锁

四个必要条件:互斥、请求并持有、不可剥夺、循环等待。治理方法包括固定加锁顺序、缩短临界区、避免嵌套锁和使用超时获取。

活锁

线程不断响应彼此、状态一直变化,却没有实际进展。随机退避可以缓解部分活锁。

饥饿

某线程长期得不到 CPU 或锁。公平策略可以缓解,但会牺牲一定吞吐。

锁内慢操作

不要在锁中执行不可控远程调用、慢 SQL、文件 I/O 或大计算。它们会把外部延迟放大为所有竞争线程的等待。

十二、高频面试问题

  1. synchronized 锁实例方法和静态方法有什么区别?
  2. Monitor 的 Owner、EntryList、WaitSet 分别是什么?
  3. notify 后线程为什么不能立即执行?
  4. synchronized 与 ReentrantLock 如何选择?
  5. 公平锁为什么吞吐通常更低?
  6. Condition await/signal 的流程是什么?
  7. 读写锁为什么不支持直接升级?
  8. StampedLock 乐观读为什么必须先读再 validate?
  9. 偏向锁是否仍存在于所有现代 JDK?
  10. 如何排查 Java 死锁?

参考资料

DISCUSSION

评论与讨论

留下你的想法