跳转到正文
Siolin'Log
返回

Java AQS:同步状态、等待队列与阻塞唤醒

概述

AQS 同步状态与等待队列概览

AQS 是 Java 并发包中构建锁和同步器的基础框架。

可以把它理解成:AQS 使用一个同步状态 state、一个等待队列以及 CAS,统一解决“资源竞争失败后,线程如何排队、阻塞、唤醒和重新竞争”的问题。

ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLock 等都建立在 AQS 之上。

解决什么问题

只有 CAS 时,可以这样竞争资源:

while (!compareAndSetState(0, 1)) {
    // 不断重试
}

竞争激烈时,失败线程会一直自旋,浪费 CPU(即自旋锁)。

更合理的方式是:

尝试获取资源

获取成功:继续执行

获取失败:进入等待队列并阻塞

资源释放:唤醒等待线程

被唤醒后:重新竞争资源

这就是 AQS 处理的核心流程:先尝试获取,失败后排队,并在必要时挂起线程。

AQS 将通用流程封装起来:

具体同步器只需要定义:

核心组成

同步状态 state

AQS 内部维护一个整数同步状态:

private volatile int state;

在 ReentrantLock 中:

state == 0    锁没有被持有
state == 1    当前线程持有一次
state == 2    当前线程重入两次

在 Semaphore 中:

state = 当前剩余许可证数量

因此,state 没有统一的业务含义。它只是 AQS 提供的一块同步状态存储空间。

AQS 提供了一些修改方法:

protected final int getState();

protected final void setState(int newState);

protected final boolean compareAndSetState(
        int expect,
        int update
);

同步等待队列

当线程获取同步资源失败时,AQS 会将其加入同步等待队列。节点按入队关系组织,但最终获取顺序还取决于子类的获取策略;使用 AQS 并不自动意味着严格公平。

逻辑上可以看成是双向链表

获取和释放模板

AQS 将同步操作分成两种模式。

独占模式:同一时刻通常只有一个线程成功。

共享模式:同一时刻可能允许多个线程成功。

模板方法设计

AQS 定义了完整的排队、阻塞和唤醒流程,但把资源判断逻辑留给子类实现。

子类主要覆盖以下方法:

protected boolean tryAcquire(int arg);

protected boolean tryRelease(int arg);

protected int tryAcquireShared(int arg);

protected boolean tryReleaseShared(int arg);

protected boolean isHeldExclusively();

AQS 对外的通用流程类似:

public final void acquire(int arg) {
    if (!tryAcquire(arg)) {
        // 加入队列、阻塞、唤醒后重试
    }
}

这里体现了职责分离:

tryAcquire()
    子类决定是否成功

入队、阻塞、唤醒、重试
    AQS 统一完成

独占模式获取资源

ReentrantLocklock() 为例,该方法最终会进入类似:

acquire(1);

完整流程:

调用 acquire(1)

调用子类 tryAcquire(1)

是否获取成功?
    ├─ 成功:直接返回
    └─ 失败:创建等待节点

            加入同步队列

            循环检查前驱

            满足条件时再次 tryAcquire

            仍失败则 park

            被唤醒后继续循环

第一次尝试获取锁:通过 CAS 修改 state

if (compareAndSetState(0, 1)) {
    setExclusiveOwnerThread(currentThread);
    return true;
}

如果锁已经被持有,那需要判断是否重入

if (currentThread == getExclusiveOwnerThread()) {
    int nextState = state + acquires;
    setState(nextState);
    return true;
}

如果锁被其他线程持有,即 线程 B tryAcquire() 失败,AQS 会为线程 B 创建节点并加入同步队列

假设队列原来是:

head → 节点 C ← tail

线程 B 加入后:

head → 节点 C → 节点 B ← tail

进入同步队列后并不会立刻阻塞,而是进入一个循环:

检查自己的前驱节点

如果前驱是 head

再次尝试获取资源

在典型的独占获取流程中,通常由 head 的后继节点再次尝试获取资源,从而避免所有等待线程同时竞争。

线程阻塞

AQS 底层主要通过以下两个方法来阻塞和唤醒线程:

LockSupport.park();
LockSupport.unpark(thread);

park

暂停当前线程:

LockSupport.park(this);

线程会停止继续占用 CPU,直到发生以下情况之一:

park() 返回不代表线程已经获得锁,只代表:线程可以醒来,再次检查同步状态。

因此 AQS 必须使用循环:

for (;;) {
    if (tryAcquire(arg)) {
        return;
    }

    park();
}

unpark

释放锁时,AQS 会找到合适的后继节点:

LockSupport.unpark(successor.thread);

被唤醒线程从 park() 返回,然后重新执行:

tryAcquire(arg);

如果仍然失败,它可能再次进入阻塞。



上一篇
xv6 Trap 机制
下一篇
Java Condition:条件等待、通知与队列转移