Java并发:synchronized、AQS、ReentrantLock与Condition
前言
锁用于保护共享可变状态。先界定“哪一份数据必须一起变化”,再选择锁;不能把锁当作解决所有一致性问题的万能工具。跨实例的最终正确性仍要依赖数据库约束、事务与幂等。
synchronized 与 ReentrantLock
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 使用方式 | JVM 内置监视器,自动释放 | 显式 lock()/unlock(),必须 finally 释放 |
| 可中断获取 | 不直接支持 | lockInterruptibly() 支持 |
| 超时获取 | 不直接支持 | tryLock(timeout) 支持 |
| 多条件队列 | 一个监视器一个 wait set | 多个 Condition 可区分队列 |
| 公平性 | 无公平锁选择 | 可选公平/非公平 |
| 适用建议 | 简短临界区、语义简单 | 需要超时、中断、多条件或可观测能力 |
1 | private final ReentrantLock lock = new ReentrantLock(); |
不要在持锁期间调用 HTTP、等待 MQ 回调、查询大批数据或做长时间计算。锁持有越久,排队和死锁风险越高。
AQS 是什么
AbstractQueuedSynchronizer(AQS)是许多同步器的基础框架。它用一个 state 表示同步状态,用 FIFO 等待队列管理获取失败的线程。ReentrantLock、Semaphore、CountDownLatch 等都可基于它实现,但共享状态含义不同。
flowchart LR
A[线程尝试 CAS 获取 state] --> B{成功?}
B -->|是| C[进入临界区]
B -->|否| D[加入 AQS 等待队列并 park]
E[持有者释放 state] --> F[unpark 后继节点]
F --> A
ReentrantLock 的 state 可表示重入次数;Semaphore 的 state 是剩余许可;CountDownLatch 的 state 是未完成任务数。理解 AQS 的关键不是背节点字段,而是“先 CAS 快速获取,失败后排队和阻塞,释放时唤醒后继”。
可重入、公平与读写锁
可重入意味着同一线程已获得锁后可再次获得,内部维护持有线程和重入次数,避免递归调用自己把自己锁死。
公平锁优先让等待更久的线程获取锁,延迟更可预测但吞吐常较低;非公平锁允许新线程抢占,通常吞吐更高。大多数业务默认非公平即可,除非确实存在长期饥饿且能接受吞吐损失。
ReentrantReadWriteLock 适合读远多于写、读临界区足够长且数据确实需要锁保护的场景。读锁可并发,写锁互斥;写频繁或临界区很短时,额外协调成本未必划算。读写锁不能替代数据库事务。
Condition 与生产者消费者
Condition 把一个锁上的等待拆成多个条件队列,典型是“队列非空”和“队列未满”:
1 | while (queue.isEmpty()) { |
必须用 while 而不是 if,因为线程被唤醒后不代表条件仍然成立,可能存在虚假唤醒或其他线程先一步消费。实际生产者消费者优先选 BlockingQueue,除非确实需要自定义同步语义。
死锁与治理
死锁的四个必要条件是互斥、占有并等待、不可抢占、循环等待。应用最常见的是两个线程以相反顺序获取两把锁。
sequenceDiagram
participant A as 线程 A
participant B as 线程 B
A->>A: 持有锁 1,等待锁 2
B->>B: 持有锁 2,等待锁 1
Note over A,B: 循环等待,无法继续
治理手段:统一加锁顺序;合并锁粒度;使用 tryLock 超时失败并回退;缩短临界区;用 jstack 或 JFR 查找 BLOCKED 线程和锁拥有者。不可通过无限重试掩盖死锁。
高频面试题
| 问题 | 回答要点 |
|---|---|
| AQS 做了什么? | state + CAS + FIFO 等待队列,统一实现独占/共享同步获取与释放。 |
| 为什么 unlock 放 finally? | 异常时仍要释放,否则其他线程永久等待。 |
| 何时用 ReentrantLock? | 需要中断、超时、公平或多个 Condition;简单短临界区可用 synchronized。 |
| Java 锁能否保证分布式一致性? | 不能,只在一个 JVM 内协调线程;跨实例需数据库、Redis/ZK 等并保留幂等与约束。 |


