前言

锁用于保护共享可变状态。先界定“哪一份数据必须一起变化”,再选择锁;不能把锁当作解决所有一致性问题的万能工具。跨实例的最终正确性仍要依赖数据库约束、事务与幂等。

synchronized 与 ReentrantLock

维度 synchronized ReentrantLock
使用方式 JVM 内置监视器,自动释放 显式 lock()/unlock(),必须 finally 释放
可中断获取 不直接支持 lockInterruptibly() 支持
超时获取 不直接支持 tryLock(timeout) 支持
多条件队列 一个监视器一个 wait set 多个 Condition 可区分队列
公平性 无公平锁选择 可选公平/非公平
适用建议 简短临界区、语义简单 需要超时、中断、多条件或可观测能力
1
2
3
4
5
6
7
8
9
10
private final ReentrantLock lock = new ReentrantLock();

void update() {
lock.lock();
try {
updateSharedState();
} finally {
lock.unlock();
}
}

不要在持锁期间调用 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
2
3
while (queue.isEmpty()) {
notEmpty.await();
}

必须用 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 等并保留幂等与约束。