synchronized 用得好好的,为什么要用 ReentrantLock?"可中断、可超时、公平锁、多条件变量"这些名词背得滚瓜烂熟,但一问底层怎么实现的,就只剩"基于 AQS"五个字。
这篇把 ReentrantLock 和它背后的 AQS(AbstractQueuedSynchronizer)从源码层面拆开,看完你就知道锁到底是怎么排队的、公平和非公平差在哪、Condition 是怎么做到"精准唤醒"的。
一、AQS 是什么:一把锁的核心三件套
AQS 本质上是一个同步器框架,核心只有三样东西:
java
// 1. 状态位:volatile int state(0=没锁,>0=被持锁)
private volatile int state;
// 2. CLH 变体双向队列(排队等锁的线程)
static final class Node {
volatile Thread thread; // 排队的线程
volatile Node prev; // 前驱
volatile Node next; // 后继
volatile int waitStatus; // 节点状态
}
// head 指向队首(持有锁或即将持有),tail 指向队尾
private transient volatile Node head;
private transient volatile Node tail;
一句话:state 记录"锁被占了几次",队列记录"谁在等"。所有同步器(锁、信号量、栅栏)都复用这套骨架。
二、公平锁 vs 非公平锁:只差一行
ReentrantLock 默认非公平。看源码:
java
// 非公平锁加锁
final void lock() {
if (compareAndSetState(0, 1)) // 来了就抢(不管队列里有没有人等)
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 公平锁加锁
final void lock() {
acquire(1);
}
// acquire → tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 公平锁:必须队列为空才抢;非公平锁:直接抢
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
...
}
非公平 :新线程来先 CAS 抢一次,抢不到才排队;公平 :先看队列有没有人等,有人就老实排队。所以非公平吞吐高(减少上下文切换),但可能"插队",饥饿风险小但长期排队者可能晚一点拿到。
三、获取锁失败怎么办:acquire 全流程
java
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 1. 尝试获取(公平/非公平差异在这)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 失败→入队→排队自旋
Thread.currentThread().interrupt();
}
拆解:
第一步:入队(addWaiter)
java
private Node addWaiter(Node mode) {
Node node = new Node(mode); // 包装当前线程
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { // CAS 挂到队尾
pred.next = node;
return node;
}
}
enq(node); // 队列为空或 CAS 失败,用自旋入队
return node;
}
第二步:排队等待(acquireQueued)
java
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { // 前驱是队首 → 再抢一次
setHead(node); // 拿到锁,自己变队首
p.next = null; // 出队
return interrupted;
}
// 没拿到 → 挂起线程(park),等前驱唤醒
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
}
核心 :每个节点在自己的前驱是 head 时才有资格尝试获取锁------这就是队列的 FIFO 特性(非公平也只在入队前插队一次,入队后就老实排队)。
四、释放锁:唤醒后继
java
public final boolean release(int arg) {
if (tryRelease(arg)) { // state 减到 0 才算释放
Node h = head;
if (h != null) {
LockSupport.unpark(h.next.thread); // 唤醒队首的下一个
}
return true;
}
return false;
}
关键 :tryRelease 里 state 减到 0 才真正释放(可重入时 state > 0 只是减一,锁还在)。唤醒的永远是 head 的下一个节点。
五、可重入:state 是怎么玩出花的
java
// 可重入:同一线程再次 lock
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { ... } // 无人持有 → 直接拿
else if (current == getExclusiveOwnerThread()) { // 自己已持有
int nextc = c + acquires; // state + 1
setState(nextc); // 重入+1
return true;
}
return false; // 别人持有 → 失败
}
state 就是重入计数:0=无锁,1=持有一层,2=同一线程重入两次......每次 unlock 减一,减到 0 才真正释放唤醒后继。
六、Condition:精准唤醒怎么实现
synchronized 只有 wait/notify 一个等待集;ReentrantLock 可以 new 多个 Condition,每个 Condition 一条独立等待队列。
java
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
condition.await():当前线程释放锁,加入该 condition 的等待队列,挂起condition.signal():把等待队列队首一个节点移到锁的同步队列,等它重新竞争signalAll():全部移到同步队列
对比 synchronized:
| 能力 | synchronized | ReentrantLock + Condition |
|---|---|---|
| 锁释放自动 | 是 | 否(finally unlock) |
| 可重入 | 是 | 是 |
| 中断响应 | 否 | 是 |
| 超时等待 | 否 | tryLock(3, SECONDS) |
| 条件变量 | 1 个 | 多个 Condition |
| 公平性 | 非公平 | 可配置 |
七、总结
- AQS 三件套:state + 双向等待队列 + CAS,是所有同步工具的地基。
- 公平/非公平:公平 = 入队前看队列;非公平 = 先 CAS 抢一次。入队后都 FIFO。
- 获取流程:tryAcquire → 入队 → 自旋检查前驱是 head → park。
- 释放流程:state 减到 0 → unpark 后继。
- 重入:state 就是重入计数,减到 0 才真正释放。
- Condition:每个 Condition 一条等待队列,精准唤醒。
下一篇:synchronized 和 volatile 的底层原理,JMM 三大特性是怎么落到 CPU 缓存上的。