一、简介
当我们谈到并发的时候,就会想到java中juc包下的一系列的并发工具,例如Lock、CountDownLatch、CyclicBarrier以及Semphore等,当你使用这些并发工具的时候,就必需深刻理解AQS,因为AQS是juc下并发工具的灵魂,也就是他们的上层框架。
AQS全称为AbstractQueuedSynchronizer,也就是抽象队列同步器,定义了一套多线程访问共享资源的同步器框架。在使用者的角度来讲,AQS的功能可以分为两类:独占模式和共享模式。它的所有子类中,要么实现并使用了独占模式的API,要么使用了共享模式的API,而不会同时使用两套API,即便是ReentrantReadWriteLock,也是通过两个内部类:读锁和写锁,分别实现的两套API来实现的。
可以知道,AQS在功能上主要有独占模式和共享模式两种功能。队列以及任务处理已经在AQS内部设计处理好了,子类也就是自定义同步器只需要按照自己的特性去重写独享或共享同步资源的处理方式即可。
二、概况
从全局来看,AQS内定义了一个共享资源,那些试图获取或者更改这个共享资源的线程,在一定条件下形成一个双向指针关联的线程节点等待链表,并且这些节点按照一定的规则来获取操作这个共享变量,也就是我们常说的获取锁和释放锁的过程,框架的大概结构如下图:
框架结构?上图展示了AQS框架的一个基本工作情况,CLH队列是一个先进先出的线程节点双向指针队列,每个节点是包含线程以及一些指针的Node类对象,互相通过指针关联成链表。state是共享资源,在AQS类中是通过volatile修饰的int类型的变量代表的。我们可以观察到头结点目前处于获取资源成功的状态,也就是操作state成功,而后续节点在队列中等待获取该资源。
对于共享资源state的访问方式,在AQS中提供了三个方法:
//获取共享资源
protected final int getState() {
return state;
}
//操作共享资源
protected final void setState(int newState) {
state = newState;
}
//cas设置共享资源
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS中定义了两种资源共享方式:
1.独享资源方式(EXCLUSIVE),比如ReentrantLock就是使用独享资源方式API实现的锁工具;
2.共享资源方式(SHARED),例如CountDownLatch、Semphore等就是通过AQS的共享API实现的并发工具;
对于两种模式,AQS分别暴露出了可以重写的方法API,方法如下:
//独享获取资源
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//独享释放资源
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//共享获取资源
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//共享释放资源
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
//线程是否整在独占资源,Condition使用
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
独享资源方式(EXCLUSIVE):比如Lock的ReentrantLock,共享资源state初始化为0,表示资源未锁定状态,当T1线程掉用lock()进行资源锁定时,会调用tryAcquire()独占该资源即将state+1,代表共享资源处于锁定状态。此后,其他线程再tryAcquire()时就会失败,直到T1线程掉用unlock()方法,释放共享资源,即state=0(释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,T1线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
共享资源方式(SHARED):以CountDownLatch为例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,共享资源state会cas减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数处唤醒,继续后余动作。
一般来讲,自定义同步器要么是独占方式,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一对即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock,但是其提供API也是两种分开提供。
三、解析
3.1 独占模式
acquire()方法是独占模式下的顶层入口,源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看到大致的逻辑:
1.tryAcquire()尝试获取资源,如果获取成功直接返回;
2.如果获取资源失败,则addWaiter()方法创建新的独占模式下的node节点,放在队列尾部;
3.调用acquireQueued()方法尝试获取资源,直到获取到资源返回;
4.获取资源过程中不响应中断,如果获取资源过程中被中断过,在获取到资源之后,自我补上中断;
首先我们看下tryAcquire(),此方法尝试去获取独占资源,如果获取成功,则直接返回true,逻辑如下:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
直接抛出异常?刚才提到AQS是一个同步器框架,上层关于队列的实现已经处理好,只需要工具关注获取操作共享资源的方式实现即可,通过操作state的方法(get/set/cas)来操作。
如果tryAcquire()获取资源失败,则调用addWaiter()方法为此线程创建一个独占模式类型的Node节点,放在CLH队列尾部,源码如下:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//新建独享类型的Node
Node pred = tail;
if (pred != null) {//存在尾节点,则cas添加本节点至队列尾部
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);//如果没有尾节点,则该方法处理
return node;
}
首先获取尾部节点(根据tail指针),如果存在尾部节点,则cas设置该节点到队列尾部,tail指针指向该尾部节点;如果尾部节点不存在或者cas操作失败的话,则证明队列为空或者并发出现,走enq()初始化逻辑:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
enq()中心思想是cas+自旋实现节点的入队尾操作。如果没有节点(tail指针null),则初始化一个空的头结点,然后入队该节点至尾部,过程如下:
1.首先判断尾部节点是否存在;
2.若尾部节点不存在,则新建一个空的节点,cas设置head指针指向该节点,且设置tail指针指向该新建节点,继续循环;
3.若尾部节点存在(可能是2步骤建的),则证明队列初始化过,设置该节点的前一节点指针指向原tail节点,然后cas设置尾部节点为该新节点;
4.设置成功则返回,设置失败则继续循环;
本节点入队尾后,就尝试去获取共享资源,如果成功则整个返回,如果失败则进入等待状态等待唤醒,方法acquireQueued()过程如下:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {//如果前一节点是头节点,就尝试获取锁
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果前节点不是头节点或者头节点还没有释放锁,则考虑是否阻塞本线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
1.自旋开始,获取本节点的前驱节点,判断是否为头节点;
2.如果是头节点,则尝试获取锁tryAcquire(),如果获取锁成功,则设置头节点为当前节点,释放原头节点,方法返回;
3.如果前驱节点不是头节点,或者获取锁失败(头节点没有释放锁),则走进shouldParkAfterFailedAcquire()方法,根据条件是否阻塞线程;
下面我们看下shouldParkAfterFailedAcquire()方法逻辑,源码如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//前驱节点是SIGNAL状态,则本线程可以阻塞
return true;
if (ws > 0) {
//前驱节点是CANCLED状态,则循环获取waitStatus<=0的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//中间无强引用的状态为CANCLED的node链将被gc回收
pred.next = node;
} else {
//有效的前驱节点,设置状态为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
1.首先判断前节点状态是否是SIGNAL,如果是SIGNAL,则表示本节点处于安全节点后,等待前节点唤醒自己,即可以阻塞自己;
2.如果不是SIGNAL,若状态大于0(CANCLED)也就是取消状态,则此节点无效,继续往前找,直到找到状态不大于0的节点,去除这些取消状态的node(gc回收),然后继续外循环,也就是acquireQueued()的循环;
3.如果是有效状态的前节点,则cas设置前节点状态为SIGNAL,然后继续外循环,也就是acquireQueued()的循环;
本方法中心思想就是,找到一个有效的(waitStatus<=0)前驱节点,将他的状态设置为SIGNAL(或者已经是SIGNAL),则可以安心阻塞休息了,等待该前驱节点唤醒自己,从而去获取锁。
假如shouldParkAfterFailedAcquire()返回true,也就是可以休息了,则进入parkAndCheckInterrupt()方法,过程如下:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//park线程,线程进入waiting状态,等待unPark唤醒
return Thread.interrupted();//唤醒后,返回线程中断标识
}
LockSupport.park()中断此线程,等待LockSupport的unPark()方法唤醒,或者线程中断,另外需要注意Thread.interrupted()会清空线程中断标识。
acquireQueued()过程?下面我们回到acquireQueued()方法执行过程:
1.节点入队尾后,尝试获取锁,如果获取锁成功,则设置头结点为当前节点,如果中断过,则返回中断标识true;
2.如果获取锁失败,则遍历检查节点状态,找到一个安全节点;
2.调用park()方法,线程进入waiting状态,等待唤醒;
3.前驱节点释放锁后,掉用unpark()唤醒本节点后,继续流程1;
acquire()过程?介绍过了前面的具体方法执行逻辑,我们可以总结acquireQueued()方法的总体执行逻辑如下简述:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1.首先,调用自定义同步器的重写的tryAcquire()方法,尝试获取共享资源,如果成功直接返回;
2.如果获取资源失败,调用addWaiter()方法,为当前线程新建独占模式的节点加入CLH队尾;
3.之后调用acquireQueued()方法,使线程在队列休息(waiting),满足条件则被唤醒,获取资源后,返回是否中断标识;
4.如果过程中被中断过,则返回true,调用selfInterrupt()方法补上自我中断;
acquire()方法(独占模式获取资源)流程图如下所示:
以上是acquire()独享模式的大致逻辑。
release()?下面看下独占模式下释放资源的上层入口,release()方法的执行逻辑,源码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
1.首先调用方法tryRelease()去释放锁,如果失败直接返回false;
2.如果释放锁成功,则看下头结点的状态,如果waitStatus不等于0,则唤醒等待队列下一个线程节点;
3.如果头结点为null或者状态为0,则直接返回true;
下面我们先看下tryRelease()方法逻辑:
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
没错,还是直接抛出异常,是需要自定义同步器自己去重写逻辑的,比如Lock的unlock()方法,释放资源后state=0,则表示释放锁成功。
接下来我们看下唤醒后续线程节点的方法unparkSuccessor(),源码如下:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)//设置头节点线程状态为0
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//获取头结点的下一节点
if (s == null || s.waitStatus > 0) {//如果此节点为空或者取消状态
s = null;
//循环找到队列最前面的状态小于0(有效)的节点,赋给s
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒此节点
}
此方法逻辑比较明显,首先将头结点的状态置为0,然后找到队列中最靠前的一个waitStatus小于0的有效节点,调用unpark()唤醒它。结合上面的acquireQueued()方法逻辑,唤醒之后,此线程继续外部循环,尝试获取锁的过程(if (p == head && tryAcquire(arg))),因为此线程节点是该队列的最靠前的有效状态的节点,因此即使获取锁失败(前驱非head或头节点没有释放资源),也可以通过shouldParkAfterFailedAcquire()方法,去除掉无效的节点,继续自旋判断获取锁的时候,就会成功。
以上是独占模式下的获取资源和释放资源的AQS框架逻辑,中心逻辑就是通过acquire()方法尝试获取共享资源操作,失败则通过队列阻塞处理;通过独占模式下线程释放共享资源release()方法,释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他节点线程来获取资源。
3.2 共享模式
AQS共享模式的入口方法为acquireShared(),其源码如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
首先通过tryAcquireShared()方法获取资源,如果成功则直接返回,如果失败则调用方法doAcquireShared()进入队列等待,被唤醒后尝试获取锁。在AQS中定义了tryAcquireShared()获取锁的返回结果标识,负数代表获取资源失败,0代表获取资源成功,没有剩余资源,整数代表获取资源成功,有剩余资源。
下面我们看下doAcquireShared()方法逻辑,源码如下:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//节点入队尾
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {//如果前驱是头结点,则尝试获取锁
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);//如果有剩余资源,则唤醒后续节点
p.next = null; // help GC
if (interrupted)
selfInterrupt();//如果中断,则补上中断
failed = false;
return;
}
}
//不满足条件,则阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
逻辑比较清晰,当本节点前驱节点是头结点的时候会尝试去获取资源,如果失败则回去找一个安全点,在其之后阻塞自己,如果成功获取资源之后还有剩余资源,则会通过方法setHeadAndPropagate()设置本节点为头结点,还会去唤醒后续有效节点;源码如下:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
可以看出这个方法比独享模式下的setHead()方法多了一个唤醒后续节点的逻辑。下面的doReleaseShared()方法就是共享模式唤醒后续节点的方法,此方法也被用于共享模式释放资源后唤醒后续节点,因此在后面讲解。
总的来说共享模式下获取资源的逻辑如下:
1.tryAcquireShared()尝试获取资源,成功则直接返回;
2.如果获取资源失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。
acquireShared()和acquire()的流程大同小异,不同的地方在于共享模式下自己拿到资源后,如果资源剩余,还会去唤醒后继节点。
下面我们看下共享模式下释放资源的入口逻辑,方法releaseShared()如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
如果释放资源失败,则直接返回false,如果释放资源成功,则进行后续节点的唤醒,进入doRealeaseShared(),源码如下:
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);//唤醒后续节点
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
unparkSuccessor()是和独享模式下公用的唤醒后续节点的方法。
以上是本次解析AQS的独享以及共享模式的入口及其实现机制,后面会从Lock以及一些自定义同步器工具方面,从实践来看下AQS的应用。
四、资源地址
文档:《Thinking in java》
jdk1.8版本源码