什么是AQS?
AQS(AbstractQueuedSynchronizer)是Java并发包(java.util.concurrent.locks)中的一个核心框架,用于构建锁(ReentrantLock)和其他同步器(如Semaphore、CountDownLatch)的基础。它封装了一个同步器时最复杂、最易错的部分:线程阻塞队列的管理、线程的阻塞与唤醒。
只需要定义“资源能否被获取”的规则,而“线程排队等待”的繁琐工作由AQS自动完成。
同步状态管理
AQS内部维护了一个volatile int state变量,代表共享资源的状态。
同步状态的语义由子类定义。例如,在ReentrantLock中,state=0表示锁空闲,state>0表示锁被持有,且数值表示重入次数;在Semaphore中,state表示可用许可证数量。
相关的api:
protected final int getState(): 获取当前状态值。
protected final void setState(int newState): 设置状态值。
protected final boolean compareAndSetState(int expect, int update): 核心,通过CAS原子操作更新状态,保证并发安全。
需要子类实现的关键方法:
protected boolean tryAcquire(int arg): 尝试以独占模式获取资源。成功返回true,失败返回false。arg是获取资源的数量,通常为1。
protected boolean tryRelease(int arg): 尝试释放独占模式的资源。成功返回true。
protected int tryAcquireShared(int arg): 尝试以共享模式获取资源。返回负值表示失败;0表示成功,但无剩余资源;正值表示成功,且有剩余资源。
protected boolean tryReleaseShared(int arg): 尝试释放共享模式的资源。
供外部调用的模板方法:
子类实现上述tryXXX方法后,外部就可以调用AQS提供的以下模板方法,这些方法内部会调用你实现的tryXXX方法,并负责复杂的队列管理。
public final void acquire(int arg): 获取资源的主入口。如果tryAcquire成功则直接返回,否则线程会进入等待队列,可能被反复阻塞和唤醒,直到成功。不可中断。
public final boolean release(int arg): 释放资源。调用tryRelease成功后,会唤醒队列中一个等待线程。
public final void acquireShared(int arg)
public final boolean releaseShared(int arg)
一个FIFO线程等待队列
CLH队列是一种经典的、用于实现高效自旋锁的同步队列数据结构。其核心设计目标是在共享内存多处理器系统上,减少锁竞争带来的总线流量和缓存一致性开销,从而实现高性能的锁机制。
可以将它理解为一个隐式的、虚拟的“排队队列”,其核心思想是:每个试图获取锁的线程,只监听(自旋检查)其前驱线程的状态,而不是全局的锁状态。
-
队列节点: 每个竞争锁的线程都会持有一个自己的节点(
QNode)。节点中最关键的字段是一个布尔值locked(或isLocked)。locked = true: 表示该线程正在持有锁,或者正在等待锁(即它还没有获取到锁,但已经在队列中)。locked = false: 表示该线程已经释放了锁,后继线程可以获取锁了。
-
队列结构:
- 队列有一个“尾巴”指针(
tail),这是一个可以被所有线程访问的原子变量。 - 当一个新线程尝试获取锁时,它会创建一个新节点,并通过原子操作(如
getAndSet或compareAndSwap)将自己设置为新的tail,同时获取到旧的tail(即它的前驱节点)。 - 线程们通过这个“获取前驱”的操作,隐式地形成了一个链表式的队列,但节点之间并没有真正的
next指针连接,而是通过操作tail的顺序来逻辑链接。
- 队列有一个“尾巴”指针(
自旋等待:
- 线程在成功将自己加入队列尾部后,不会去轮询一个全局的锁标志,而是持续地自旋检查其前驱节点的
locked字段。 - 只要前驱节点的
locked为true,它就继续等待(自旋)。 - 一旦检测到前驱节点的
locked变为false,就意味着前驱线程已经释放了锁,自己可以获取锁并开始执行临界区代码。
释放锁:
- 线程执行完临界区代码后,将自己节点的
locked设置为false。 - 这样,正在自旋监听它的后继线程就会立即发现这个变化,并结束等待,成功获取锁。
对比CAS锁的优势:
-
减少缓存一致性流量(核心优势):
- 在传统自旋锁中,所有等待线程都在自旋读取同一个内存位置(锁标志)。这会导致严重的“缓存一致性”问题,每次锁状态变化都会使所有CPU核心的缓存失效,产生巨大的总线流量,严重影响性能。
- 在CLH锁中,每个线程自旋检查的是自己前驱节点(一个局部变量或独立内存块)的状态。这个状态通常位于线程的本地缓存或邻近核心的缓存中,通信开销极小。锁释放时,只需要修改一个变量来通知一个后继线程,极大地减少了全局内存争用。
-
保证公平性:
- CLH锁是一个严格的先来先服务(FIFO) 队列,绝对公平。这可以防止线程“饿死”。
AQS中的FIFO队列就是对CLH队列的一种变体。
工作模式
AQS支持两种资源访问模式:
- 独占模式:资源一次只能被一个线程持有。
- 共享模式:资源可以同时被多个线程持有。
两种模式的线程在同一个队列中排队,但被唤醒和获取成功的逻辑不同。
AQS的使用
AQS使用了典型的模板方法模式,作为使用者,只需要关心状态 state的含义和变更规则。具体方式是重写以下几个保护方法:
| 方法名 | 描述 | 需要重写场景 |
|---|---|---|
tryAcquire(int) | 独占模式。尝试获取资源,成功返回true,失败返回false。 | 实现独占锁(如Mutex) |
tryRelease(int) | 独占模式。尝试释放资源,成功返回true,失败返回false。 | 实现独占锁 |
tryAcquireShared(int) | 共享模式。尝试获取资源。返回负数表示失败;0表示成功,但无剩余资源;正数表示成功,且有余量。 | 实现信号量、闭锁 |
tryReleaseShared(int) | 共享模式。尝试释放资源,如果释放后允许唤醒后续等待线程则返回true。 | 实现信号量、闭锁 |
isHeldExclusively() | 当前线程是否独占资源。用于Condition的实现。 | 实现条件变量时 |
AQS的顶级流程:
-
acquire(int arg)方法(已由AQS实现,不可重写) :- 调用子类的
tryAcquire(arg)尝试直接获取。 - 如果失败,则将当前线程加入等待队列,并可能将其挂起(
LockSupport.park)。 - 线程被唤醒后,会再次尝试
tryAcquire。
- 调用子类的
-
release(int arg)方法:- 调用子类的
tryRelease(arg)尝试释放。 - 如果释放成功,则唤醒队列中第一个等待的线程。
- 调用子类的
公平和非公平 这是AQS的一个精妙设计。默认行为是非公平的。
- 非公平:一个新来的线程(
Thread A)在acquire时,会先直接调用tryAcquire尝试“插队”,如果成功,即使队列里有等待的线程,Thread A也能立即获取锁。这提升了吞吐量,但可能导致“饥饿”。 - 公平:在重写的
tryAcquire方法中,先检查队列中是否有比自己更早等待的线程(通过hasQueuedPredecessors()方法)。如果有,则主动获取失败,乖乖去排队。这保证了严格的FIFO顺序。
Condition支持
AQS内部类 ConditionObject实现了 Condition接口,用于实现更灵活的线程等待/通知机制(类似于 Object.wait()/notify(),但更强大)。
- 一个锁可以创建多个
Condition对象。 - 只能在独占模式下使用。
await()会将当前线程放入该条件队列并释放锁;signal()会将条件队列中的一个线程转移到AQS的主阻塞队列中,等待重新获取锁。
基于AQS实现的非公平不可重入锁
/**
* 基于AQS实现的一个不可重入锁
*/
static class NoReentrantLock implements Lock{
class Sync extends AbstractQueuedSynchronizer{
/**
* 尝试获取锁
* @param arg 获取的数量,固定为1
*
* @return 锁是否获取成功
*/
@Override
protected boolean tryAcquire(int arg) {
if(arg!=1){
throw new IllegalArgumentException("arg must be 1")
}
// 通过AQS提供的cas设置State
if(compareAndSetState(0,1)){
//成功获取锁,并将当前线程设置为独占线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* 尝试释放锁
* @param arg 为释放数量,固定为1
* @return
*/
@Override
protected boolean tryRelease(int arg) {
if(arg!=1){
throw new IllegalArgumentException("arg must be 1");
}
if(getExclusiveOwnerThread()!=Thread.currentThread()){
throw new IllegalMonitorStateException("Current thread is not the lock holder");
}
// 清空锁持有线程
setExclusiveOwnerThread(null);
// 设置状态为0,因为state被volatile标记,因此后执行,防止指令重排序
setState(0);
return true;
}
/**
* 判断是否被独占
* @return
*/
@Override
protected boolean isHeldExclusively() {
return getState()==1;
}
/**
* 创建条件变量
* @return
*/
public Condition newCondition(){
return new ConditionObject();
}
}
private Sync sync=new Sync();
/**
* 获取锁
*/
@Override
public void lock() {
sync.acquire(1);
}
/**
*
* @throws InterruptedException
*/
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/**
* 尝试获取锁,立即返回结果
* @return
*/
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
/**
* 尝试获取锁,带超时
* @param time 超时时间
* @param unit 时间单位
* @return 是否获取成功
* @throws InterruptedException 如果线程被中断
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
/**
* 释放锁
*/
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
从上述的代码可以看出,AQS就是一个实现大部分代码的基础实现。
通过重写AQS的tryAcquire和tryRelease来实现Lock的tryLock与Lock等方法。