← 返回 JUC 列表

AQS — 抽象队列同步器

AQS — 抽象队列同步器

一、AQS 是什么?

知识点说明

AQS(AbstractQueuedSynchronizer) 是 JUC 的灵魂基石,几乎所有同步工具(ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock)的底层实现都依赖 AQS。

AQS = state(状态) + CLH 队列(等待队列) + CAS(原子操作)

核心思想:如果请求的共享资源空闲,则当前线程直接使用;
         如果资源被占用,则将线程封装成一个 Node 节点,放入 CLH 队列等待。

核心结构

AQS 内部结构

┌────────────────────────────────────────┐
│  state(volatile int)                   │  ← 同步状态,0=空闲,>0=被占用
│  子类通过 getState/setState/compareAndSetState 操作 │
├────────────────────────────────────────┤
│  CLH 双向队列                             │  ← 等待队列(FIFO)
│  ┌────┐    ┌────┐    ┌────┐    ┌────┐  │
│  │HEAD│ →  │Node│ →  │Node│ →  │TAIL│  │
│  │    │ ←  │    │ ←  │    │ ←  │    │  │
│  └────┘    └────┘    └────┘    └────┘  │
│             ↑                  ↑        │
│          前驱节点            后继节点     │
├────────────────────────────────────────┤
│  独占模式(EXCLUSIVE) / 共享模式(SHARED)  │  ← 两种获取方式
├────────────────────────────────────────┤
│  非公平 / 公平(由子类 tryAcquire 实现)     │
└────────────────────────────────────────┘

Node 节点的核心字段

字段 含义
waitStatus 等待状态(0/CANCELLED/SIGNAL/CONDITION/PROPAGATE)
prev 前驱节点
next 后继节点
thread 当前节点持有的线程
nextWaiter 共享/独占模式标记

waitStatus 的五种状态:

状态 含义
CANCELLED 1 线程已取消(超时或中断),不会再被唤醒
SIGNAL -1 当前节点释放锁后要唤醒后继节点
CONDITION -2 节点在 Condition 条件队列中等待
PROPAGATE -3 共享模式下,释放锁时向后传播唤醒
0 0 初始状态

二、独占模式(以 ReentrantLock 为例)

获取锁流程(acquire)

java
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&           // ① 子类实现:尝试获取锁(非公平/公平)
        acquireQueued(                // ② 获取失败,进入等待队列
            addWaiter(Node.EXCLUSIVE), arg)) {
        selfInterrupt();
    }
}
线程尝试获取锁
    │
    ▼
tryAcquire() 成功?────是──→ 直接获得锁,返回
    │
    否
    │
    ▼
addWaiter():将线程包装成 Node,加入 CLH 队列尾部
    │
    ▼
acquireQueued():自旋等待
    │
    ├── 检查前驱是否是 HEAD?
    │      是 → 再次 tryAcquire() 尝试获取
    │           ├── 成功 → 设为 HEAD,返回
    │           └── 失败 → 继续
    │      否 → 检查是否应该 park
    │
    ├── shouldParkAfterFailedAcquire()
    │      前驱是 SIGNAL?→ 返回 true,准备 park
    │      前驱是 CANCELLED?→ 跳过前驱,继续往前找
    │      前驱是 0/PROPAGATE?→ CAS 设为 SIGNAL,再试一次
    │
    └── parkAndCheckInterrupt() → LockSupport.park() 阻塞

释放锁流程(release)

java
public final boolean release(int arg) {
    if (tryRelease(arg)) {      // 子类实现:释放锁
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); // 唤醒后继节点
        return true;
    }
    return false;
}
线程释放锁
    │
    ▼
tryRelease() 成功(state == 0)
    │
    ▼
unparkSuccessor():唤醒 HEAD 的后继节点(LockSupport.unpark())
    │
    ▼
被唤醒的线程从 parkAndCheckInterrupt() 继续循环 → 尝试获取锁

三、共享模式(以 Semaphore/CountDownLatch 为例)

获取锁流程(acquireShared)

java
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)  // 子类实现:返回剩余资源数,负数=获取失败
        doAcquireShared(arg);       // 进入等待队列
}

共享模式与独占模式的区别:

  • 独占模式:一次只有一个线程获得锁
  • 共享模式:多个线程可以同时获得资源(如读锁、信号量)

释放锁流程(releaseShared)

java
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {       // 子类实现:释放资源
        doReleaseShared();             // 唤醒后继节点(PROPAGATE 传播)
        return true;
    }
    return false;
}

共享模式释放时会传播唤醒(PROPAGATE),确保多个等待线程能被依次唤醒。


四、公平锁 vs 非公平锁

非公平锁(ReentrantLock 默认)

java
// 非公平锁的 tryAcquire
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 直接 CAS 抢锁,不排队
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 可重入逻辑...
}

// 非公平锁的 lock()
final void lock() {
    // 一进来就 CAS 抢一次,抢到就用,不排队
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

公平锁

java
// 公平锁的 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;
        }
    }
    // 可重入逻辑...
}
非公平锁 公平锁
线程 arrival 时 直接抢锁 必须排队
吞吐量 高(减少线程切换)
饥饿风险 有(新线程可能一直插队)
适用场景 默认选择 要求公平调度

五、Condition 的实现原理

知识点说明

Condition 是 AQS 内部的条件等待队列Object.wait()/notify() 基于 Monitor,而 Condition.await()/signal() 基于 AQS。

结构

AQS 同步队列(CLH):        Condition 条件队列(单向链表)
┌────┐    ┌────┐    ┌────┐        ┌────┐    ┌────┐
│HEAD│ →  │Node│ →  │TAIL│        │FIRST│ → │Node│ → ...
└────┘    └────┘    └────┘        └────┘    └────┘
↑                                ↑
正在等锁的线程                    正在等条件的线程

工作流程

await() 流程:
  ① 将当前线程 Node 从同步队列移到条件队列
  ② 释放所有锁(保存 state 值)
  ③ LockSupport.park() 阻塞,等待 signal
  ④ 被 signal 唤醒后,重新竞争锁
  ⑤ 获取到锁后返回

signal() 流程:
  ① 将条件队列头部的 Node 移到同步队列
  ② 将该 Node 的 waitStatus 设为 SIGNAL
  ③ 唤醒该线程(unpark)

注意事项

  • 一个 AQS 可以有多个 Condition(如 ReentrantLock.newCondition() 可以创建多个)
  • await()/signal() 必须在 lock()/unlock() 之间调用(类似 wait/notify 必须在 synchronized 内)
  • 多 Condition 的优势:生产者/消费者可以用不同的 Condition 分别等待,避免不必要的唤醒

六、AQS 在 JUC 中的应用

同步工具 模式 state 含义
ReentrantLock 独占 0=未占用,>0=重入次数
CountDownLatch 共享 倒计数值
Semaphore 共享 剩余许可证数
ReentrantReadWriteLock 独占+共享 高16位=读锁数,低16位=写锁重入数
ThreadPoolExecutor 使用自己的 AQS(Worker 类)实现工作线程控制

七、面试常问

Q1:AQS 如何解决线程阻塞和唤醒的性能问题?

通过 CLH 自旋锁的变体:前驱节点释放锁时会主动唤醒后继节点。线程不会一直自旋(那样浪费 CPU),而是在尝试几次失败后进入阻塞(park),减少了上下文切换。

Q2:CLH 队列为什么是双向链表?

因为需要从后往前遍历。当唤醒后继节点时,如果后继节点被取消(CANCELLED),需要从 tail 往前找到一个有效的节点进行唤醒。

Q3:AQS 如何实现可重入?

子类在 tryAcquire 中判断:如果当前线程就是持有锁的线程,state 递增,返回 true。

Q4:共享模式和独占模式的区别?

  • 独占:同一时刻只有一个线程能获取到锁(如 ReentrantLock)
  • 共享:多个线程可以同时获取到锁(如 CountDownLatch/Semaphore)
  • 实际中一个同步器可以同时支持两种模式(如 ReentrantReadWriteLock 的读锁共享、写锁独占)