Java同步框架AbstractQueuedSynchronizer(AQS)原理分析

本文深入剖析了Java并发工具包中的AbstractQueuedSynchronizer(AQS)源码,介绍了AQS的基本原理、使用方法及核心组件Node的运作机制。详细解读了acquire、release、acquireShared、releaseShared等关键方法的执行流程,以及ConditionObject如何实现类似Object的wait和notify功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文参照的是JDK1.8版本的AbstractQueuedSynchronizer源码

一、引言

AbstractQueuedSynchronizer(以下简称AQS)是Java并发工具包中用于构建锁或其它同步工具的基础框架,其ReentrantLockSemaphoreCyclicBarrier都是基于这个框架实现的。它的内部维护了一个FIFO队列,在有很多线程竞争资源时,线程对象会被加入到此队列等待。AQS的父类是AbstractOwnableSynchronizer,这个父类比较简单,仅有一个Thread类型的成员变量和它的getset方法(protected权限),代表一个竞争到资源的线程(一般用于独占资源模式而不是共享资源模式)。

AQS也可以实现类似于Objectwaitnotify来等待、激活线程:AQS有一个内部类ConditionObject,实现了Condition接口,可以调用它的awaitsignal来实现waitnotify的功能。

Java并发包有很多工具类采用了这个框架:ReentrantLockSemaphoreCountDownLatch等,所以了解它的实现原理是十分必要的。

AQS的代码还是相对来说比较复杂的,建议读者首先大概先过一遍源代码,在学习了解它的原理一定要静下心来慢慢看。


二、使用方法

AQS的API(public访问权限的方法):

方法签名作用
void acquire(int)获取独占资源
void acquireInterruptibly(int) throws InterruptedException获取独占资源,但是如果检测到当前线程被中断则会抛出异常
boolean tryAcquireNanos(int, long) throws InterruptedException获取独占资源,可以指定最多阻塞的时间(单位:纳秒)
boolean release(int)释放独占资源
void acquireShared(int)获取共享资源
void acquireSharedInterruptibly(int) throws InterruptedException获取共享资源,但是如果检测到当前线程被中断则会抛出异常
boolean tryAcquireSharedNanos(int, long) throws InterruptedException获取共享资源,可以指定最多的阻塞时间(单位:纳秒)
boolean releaseShared(int)释放共享资源
boolean hasQueuedThreads()等待队列中是否有等待的线程
boolean hasContended()是否有其它线程竞争资源时失败而被加入到等待队列
Thread getFirstQueuedThread()获取等待队列头部的线程对象
boolean isQueued(Thread)判断指定线程是否在等待队列中
boolean hasQueuedPredecessors()判断当前线程是不是在队列首部
int getQueueLength()获取等待队列的长度
Collection<Thread> getQueuedThreads()将等待队列中所有的线程对象包装为一个集合返回
Collection<Thread> getExclusiveQueuedThreads()将等待队列中所有尝试获取独占资源的线程对象包装为集合返回
Collection<Thread> getSharedQueuedThreads()将等待队列中所有尝试获取共享资源的线程对象包装为集合返回
boolean owns(ConditionObject)判断ConditionObject对象是不是属于当前AQS同步器
boolean hasWaiters(ConditionObject)判断这个ConditionObject对象有没有调用await后正在等待的线程
int getWaitQueueLength(ConditionObject)获取这个ConditionObject对象包含的等待线程数量

AQS作为一个同步框架,提供了5种非finalprotected方法用于同步器实现自己的功能:

//尝试获取独占式资源
protected boolean tryAcquire(int arg);
//尝试释放独占式资源
protected boolean tryRelease(int arg);
//尝试获得共享式资源
protected int tryAcquireShared(int arg);
//尝试释放共享式资源
protected int tryReleaseShared(int arg);
//返回当前线程是否获得了该资源
protected boolean isHeldExclusively();

int类型参数的arg一般用于调整state变量,其具体意义由同步器的实现类自己定义。

这些方法由AQS的public方法负责调用(比如tryAcquire方法就会被acquire方法调用),并且在AQS的默认实现都是抛出UnsupportedOperationException异常,需要由子类根据自己的实际业务需求自定义获取资源的策略。比如ReentrantLock内部类Sync就重写了tryAcquiretryReleaseisHeldExclusively方法,Semaphore作为共享资源同步器,其内部类Sync自然就重写了tryAcquireSharedtryReleaseShared方法。


三、AQS原理分析

1
AQS内部采用了一个非阻塞式队列(由CAS算法保证线程安全性),一般拥有一个头结点和多个包含等待获取资源的线程对象结点,这些结点有序地排列着,等待着资源的释放之后获取并占有它。另外,ConditionObject也维护了自己的等待队列。
在下文中,我们称AQS维护的等待队列为AQS队列ConditionObject维护的队列称为Condition队列

AQS使用了一个int型的变量state代表资源的状态。比如在ReentrantLock中,state为0时代表没有线程持有锁,state为1时代表有线程持有了锁。对于尝试获得锁的线程而言,会通过CAS方式将其state由0设为1,代表取得了该锁。
在AQS中,和很多JDK的并发工具相似,CAS算法由Hotspot虚拟机实现(通过sun.misc.Unsafe类)。

1、Node结点

AQS通过一个内部类Node来代表一个队列的元素。

static final class Node {
	//共享资源Node模式
	static final Node SHARED = new Node();
	//独占资源Node模式
	static final Node EXCLUSIVE = null;
    
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    
	//Node状态,对应上面几种,默认为0
    volatile int waitStatus;
	//前驱结点
    volatile Node prev;
	//后驱结点
    volatile Node next;
	//等待的线程
    volatile Thread thread;
    
	//在AQS队列中为等待节点的模式,为SHARED或EXCLUSIVE
	//在Condition队列中为下一个结点
    Node nextWaiter;
}

Node实例保存了维持队列的前驱引用和后驱引用,并保存了等待线程的对象和资源模式。

Node状态解释
CANCELLED(1)当这个Node对应的线程等待超时或被中断,没有获取到资源,处于这个状态的Node会被AQS清除
默认状态(0)当这个Node初始化,或者处于正常等待状态
SIGNAL(-1)当这个Node对应的线程已经被激活获取到资源时,它会被置于头结点,如果它获取到的资源被释放,那么由它负责通知后继结点获取资源。(只有头结点会处于这个状态)
CONDITION(-2)NodeCondition队列上等待
PROPAGATE(-3)只有头结点才会处于这个状态,表示下一次的共享状态会被无条件的传播下去(用于共享资源的同步器)

先记住上述Node状态所对应的情况,Node默认状态为0

2、acquire方法

我们从acquire方法开始分析AQS的原理:

public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquire方法表示一个线程尝试获取线程独占式资源

方法执行步骤如下:

  1. 首先调用tryAcquire尝试获取资源(由子类自己实现获取资源的策略),如果获取成功,该方法结束。
  2. 如果没有获取成功,则首先调用addWaiter方法将当前线程添加到AQS队列中,然后调用acquireQueued方法等待获取资源。如果acquireQueued方法返回false,则该线程是正常获取到资源的,如果为true,则该线程被中断(interrupt),则调用selfInterrupt中断当前线程。

用流程图表示如下:
这里写图片描述

addWaiter方法,mode表示资源模式(只有两种:Node.EXCLUSIVE表示独占资源,Node.SHARED表示共享资源):

private Node addWaiter(Node mode) {
	//将当前线程包装为一个Node结点
	Node node = new Node(Thread.currentThread(), mode);
	//将尾结点引用赋给pred
    Node pred = tail;
    //如果尾结点不为null
    if (pred != null) {
	    //将新Node的前驱指针指向尾结点
        node.prev = pred;
        //利用CAS算法安全地将这个新Node添加到队列尾部
        if (compareAndSetTail(pred, node)) {
	        //将旧尾结点的后驱指针指向新Node
            pred.next = node;
            return node;
        }
    }
	//如果尾结点为null或尝试通过CAS将新Node设为尾结点没有成功,
	//则采用enq方法自旋地将新Node添加到队列尾部
    enq(node);
    return node;
}

private Node enq(final Node node) {
	//反复循环直至添加成功为止
	for (;;) {
		//将尾结点赋值给t
        Node t = tail;
        //如果队列为空
        if (t == null) {
	        //通过CAS将一个空Node作为头结点
            if (compareAndSetHead(new Node()))
	            //将空Node也作为尾结点
	            tail = head;
        } else {
	        //将新Node的前驱指针指向尾结点
            node.prev = t;
            //通过CAS将新Node作为尾结点,若失败则重新开始循环
            if (compareAndSetTail(t, node)) {
	            //尾结点的后驱指针指向新Node
                t.next = node;
                //返回旧的尾结点
                return t;
            }
        }
    }
}

addWaiter总是能够保证以线程安全且非阻塞的方式将结点添加到队列尾部。
若AQS队列为空,则添加后的队列为:
这里写图片描述
若AQS队列不为空,则一般情况下为:
这里写图片描述
添加到队列以后,再通过acquireQueued方法尝试获取资源:

final boolean acquireQueued(final Node node, int arg) {
	//是否成功获取到资源
	boolean failed = true;
    try {
	    //该node对应的线程是否被打断
        boolean interrupted = false;
        for (;;) {
	        //获得新Node前驱Node
            final Node p = node.predecessor();
            //如果排在前面的是头结点,那么这个Node是除去头结点外的第一个结点
            //这时,这个Node就有资格尝试获取资源了(尝试调用tryAcquire)
            if (p == head && tryAcquire(arg)) {
	            //将此node设为头结点(会将前驱后驱指针设置为null)
                setHead(node);
                //删除这个结点,代表出队
                p.next = null; 
                //成功拿到资源
                failed = false;
                //返回该线程是否打断
                return interrupted;
            }
            //首先调用shouldParkAfterFailedAcquire判断本次获取资源失败后是否应当阻塞当前线程,
            //如果返回true,则使当前线程进入等待状态直到该方法返回
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
	            //如果等待过程中该线程被中断,则标记为true
                interrupted = true;
        }
    } finally {
	    //如果线程等待超时或被中断则将这个Node设置为CANCELLED状态
        if (failed)
            cancelAcquire(node);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	//获取前面的Node的等待状态
	int ws = pred.waitStatus;
	//如果前面的结点获取到资源时可以通知本结点,那么返回true,执行parkAndCheckInterrupt
    if (ws == Node.SIGNAL)
        return true;
    //如果前面的Node为CANCELLED状态
    if (ws > 0) {
	    //一直往队列前面找,直到找到正常状态的Node,处于CANCELLED状态的Node在循环结束后会失去引用
        do {
	        //往队列前面找
	        pred = pred.prev;
	        //将node的前驱指针指向pred(意味着忽略掉CANCELLED状态的Node,这种状态的Node失去了引用会被GC)
            node.prev = pred;
        } while (pred.waitStatus > 0);
        //将正常状态的Node的后驱指针指向node
        pred.next = node;
    } else {
	    //通过CAS将状态设为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
	//调用park使线程进入等待状态,这里会被阻塞直到等到前面的Node通知或被interrupt
	LockSupport.park(this);
	//返回线程有没有被中断
	return Thread.interrupted();
}

Node.SIGNAL状态表示了这个结点对应的资源释放资源后会主动通知队列后面的线程。
shouldParkAfterFailedAcquire从方法名可以看出,该方法的作用是当获取资源失败时是否应该使该线程等待,该方法同时也会清除掉这个Node前面处于CANCELLED状态的Node
parkAndCheckInterrupt方法会使当前线程进入阻塞等待状态,脱离阻塞状态后,会判断当前线程有没有被interrupt。如果调用的是acquireInterruptibly方法,那么如果这时Thread.interrupted()返回true,那么会抛出InterruptedException

acquireQueued方法的执行流程如下:

  1. 首先判断该Node是否在队列最前端(不包括头结点),如果不是跳到步骤2,如果是则调用tryAcquire尝试获取资源,成功获取到资源后将此Node出队,方法结束。如果没有