Concurrent包源码解读之AbstractQueuedSynchronizer
1. AQS 设计思想
AbstractQueuedSynchronizer–AQS用来实现锁或其他同步组件的基础框架(注意区别synchronized是在字节码上加指令方式,通过底层机器语言保证同步)。AQS使用int类型的volatile变量维护同步状态(state),使用Node实现FIFO队列来完成线程的排队执行。在锁的实现中通过组合AQS对象的方式使用,利用AQS实现锁的语义。AQS内部维护一个CLH队列来管理锁。
线程会首先尝试获取锁,如果失败,则将当前线程以及等待状态等信息包成一个Node节点加到同步队列里。
接着会不断循环尝试获取锁(条件是当前节点为head的直接后继才会尝试),如果失败则会阻塞自己,直至被唤醒;
而当持有锁的线程释放锁时,会唤醒队列中的后继线程。
AQS与锁(如Lock)的相对比:锁是面向使用者的,锁定义了用户调用的接口,隐藏了实现细节;AQS是锁的实现者,通过用AQS简化了锁的实现屏蔽了同步状态管理,线程的排队,等待唤醒的底层操作。简而言之,锁是面向使用者,AQS是锁的具体实现者。它主要是做三件事:
- 同步状态的管理
- 线程的阻塞和唤醒
- 同步队列的维护
2. AQS框架调用方式
AQS实现了一直阻塞锁或者同步工具,这种阻塞锁或者同步工具依赖先进先出等待队列。这个类的设计是大部分同步工具的基础,这些同步工具依赖一个原子值tt来表示状态。子类必须定义一个保护方法来修改这个状态,这个状态定义了这个对象正在被获取或者被释放的意义。考虑到这些,这个类中的其他方法排除所有排队和阻塞机制。子类可以维护其他状态字段,但是只有自动更新的被getState(),setState(),compareAndSetStatet()方法操作的值对同步进行跟踪。AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。AQS的设计基于模版方法,使用者继承这个abstract AQS,并重写其中的方法。AQS提供了如下final方法,与同步状态交互。state的访问方式有三种:
- getState() 获取当前同步状态
protected final int getState() {
return state;
}
- setState() 设置当前同步状态
protected final void setState(int newState) {
state = newState;
}
- compareAndSetState()调用unsafe底层C语言,保证原子性的改变同步状态。Unsafe的操作粒度不是类,而是数据和地址。
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。AQS中可选择重写的方法如下:
- tryAcquire(int):独占式获取同步状态.独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
- tryRelease(int):独占式释放同步状态.独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
- tryAcquireShared(int):共享式获取同步状态,返回值>=0表示成功,否则失败。共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
- tryReleaseShared(int):共享式释放同步状态.共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
- isHeldExclusively():AQS是否被当前线程独占.该线程是否正在独占资源。只有用到condition才需要去实现它。
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
可以看到,这些方法直接throw异常。AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实!!!AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)!!!至于能不能重入,能不能加塞,那就看具体的自定义同步器怎么去设计了!!!当然,自定义同步器在进行资源访问时要考虑线程安全的影响。 这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。说到底,Doug Lea还是站在咱们开发者的角度,尽量减少不必要的工作量。
3. AQS源码详解
分析AQS源码,主要从以下几个分类来剖析AQS各种模式获取锁的流程:
- 不响应中断的独占锁
- 响应中断的独占锁
- 不响应中断的共享锁
- 响应中断的共享锁
AQS依赖内部的同步队列(FIFO双向队列)来完成同步,当前线程获取同步状态失败时,同步器会将当前线程的引用以及等待信息构造成一个Node节点对象,并加入同步队列中,同时阻塞这个线程。当同步状态释放,会把首节点的线程唤醒,使其再次获取同步状态。Node数据结构定义如下:
static final class Node {
/** 用于标记一个节点在共享模式下等待**/
static final Node SHARED = new Node();
/**用于标记一个节点在独占模式下等待**/
static final Node EXCLUSIVE = null;
/**表示当前的线程被取消**/
static final int CANCELLED = 1;
/**表示当前节点的后继节点包含的线程需要运行,也就是unpark**/
static final int SIGNAL = -1;
/**表示当前节点在等待condition,也就是在condition队列中**/
static final int CONDITION = -2;
/*表示当前场景下后续的acquireShared能够得以执行*/
static final int PROPAGATE = -3;
/**等待状态*/
volatile int waitStatus;
/**前驱节点*/
volatile Node prev;
/**后继节点*/
volatile Node next;
/** 节点对应的线程**/
volatile Thread thread;
/** 等待队列中的后继节点**/
Node nextWaiter;
/** 当前节点是否处于共享模式等待**/
final boolean isShared() {
return nextWaiter == SHARED;
}
/** 获取前驱节点,如果为空的话抛出空指针异常**/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null) {
throw new NullPointerException();
} else {
return p;
}
}
Node() {
}
/**addWaiter会调用此构造函数**/
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
/**Condition会用到此构造函数**/
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
值与状态描述对于表格如下:
值 | 描述 |
---|---|
CANCELLED (1) | 值为1,表示当前的线程被取消 |
SIGNAL (-1) | 值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark |
CONDITION (-2) | 值为-2,表示当前节点在等待condition,也就是在condition队列中 |
PROPAGATE (-3) | 值为-3,表示当前场景下后续的acquireShared能够得以执行 |
0 | 值为0,表示当前节点在sync队列中,等待着获取锁 |
3.1 不响应中断的独占锁获取锁的流程
不响应中断的独占锁获取锁使用acquire(int),acquire(int)函数流程如下:
1. tryAcquire()尝试直接去获取资源,如果成功则直接返回;
2. addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
3. acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
acquire(int)函数的定义如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire方法前面说过了,是子类实现的一个方法,如果tryAcquire返回的是true,即表明当前线程获得了锁,自然也就不需要构建数据结构进行阻塞等待。如果tryAcquire方法返回的是false,那么当前线程没有获得锁,接着执行”acquireQueued(addWaiter(Node.EXCLUSIVE), arg))”这句代码,这句话很明显,它是由两步构成的:
1. addWaiter(Node.EXCLUSIVE), arg),将当前线程封装成一个节点,添加到“等待锁的线程队列”中去。
2. acquireQueued,当前线程所在节点目前在“等待锁的线程队列”中,当前线程仍然尝试从等待队列中去获取锁。
第一:具体分析addWaiter(Node.EXCLUSIVE), arg)流程:
/**在队列中新增一个节点**/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 快速尝试
if (pred != null) {
node.prev = pred;
// 通过CAS在队尾插入当前节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
/**初始情况或者在快速尝试失败后插入节点**/
enq(node);
return node;
}
独占模式下,给addWaiter传递的参数是Node.EXCLUSIVE。并且获取当前线程thread,将当前线程thread以及EXCLUSIVE独占模式,构造成一个节点Node。构造完成之后,需要入队,即加入到“等待锁的线程队列CLH”中。如何入队?首先尝试的是快速入队。何为快速入队?直接把我们刚才构造的node的前驱指针指向当前尾节点,然后通过CAS操作把我们刚才构造的node作为新的尾节点,最后再把原来老的尾节点的后继指针指向现在的新的尾节点。说了那么多,那么快速入队的大前提是什么?那就是这个“等待锁的线程队列CLH”必须先存在。如果不存在,那么只能走常规的入队操作流程,也就是进入到enq(node)方法中。从这里我们也可以知道,其实enq(node)在AQS的整个生命周期中只会执行一次,因为只有第一次初始化“等待锁的线程队列CLH”才会走到这里,后来的入队都会走“快速入队”流程。假设我们这里还没有“等待锁的线程队列CLH”,即当前的tail节点为null,那么就会进入enq(node)方法。 下面我们看下enq(node)方法的源代码:
/**通过循环+CAS在队列中成功插入一个节点后返回。**/
private Node enq(final Node node) {
//CAS"自旋",直到成功加入队尾
for (;;) {
Node t = tail;
if (t == null) {// 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
if (compareAndSetHead(new Node()))
tail = head;
} else {
/*
* AQS的精妙就是体现在很多细节的代码,比如需要用CAS往队尾里增加一个元素
* 此处的else分支是先在CAS的if前设置node.prev = t,而不是在CAS成功之后再设置。
* 一方面是基于CAS的双向链表插入目前没有完美的解决方案,另一方面这样子做的好处是:
* 保证每时每刻tail.prev都不会是一个null值,否则如果node.prev = t
* 放在下面if的里面,会导致一个瞬间tail.prev = null,这样会使得队列不完整。
*/
node.prev = t;
// CAS设置tail为node,成功后把老的tail也就是t连接到node。
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这段代码的逻辑为:
如果尾节点为空,即当前数据结构中没有节点,那么new一个不带任何状态的Node作为头节点,并且将head赋值给tail。如果尾节点不为空,那么并发下使用CAS算法将当前Node追加成为尾节点,由于是一个for(;;)循环,因此所有没有成功acquire的Node最终都会被追加到数据结构中。
第二:具体分析acquireQueued(final Node node, int arg)流程:
刚才我们分析addWaiter(Node.EXCLUSIVE), arg)流程,知道了当前线程已经被封装成了Node节点加入到了“等待锁的线程队列CLH”中了,但是当前线程目前还没有阻塞。那么它是什么时候应该阻塞呢?它在阻塞之前应该做些什么呢?下面我们具体分析。来看下acquireQueued(final Node node, int arg)源码:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//标记是否成功拿到资源
try {
boolean interrupted = false;//标记等待过程中是否被中断过
for (;;) {//又是一个“自旋”!
final Node p = node.predecessor();//拿到前驱
//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
if (p == head && tryAcquire(arg)) {
setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
failed = false;
return interrupted;//返回等待过程中是否被中断过
}
//如果自己可以休息了,就进入waiting状态,直到被unpark()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
}
} finally {
if (failed)
cancelAcquire(node);
}
}
通过前面的分析,我们知道如果通过tryAcquire获取失败之后,就会进入到addWaiter(Node.EXCLUSIVE), arg)流程,进而进入到acquireQueued(final Node node, int arg)流程。进入到这个流程,就说明了该线程在之前是获取资源失败的,已经被放入等待队列尾部了。聪明的你立刻应该能想到该线程下一部该干什么了吧:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。没错,就是这样!但是在休息之前,该线程还是不泄气,它还是想再争取一次。
情况一:如果当前线程所在节点的前继节点是head节点,那么当前节点就再次的tryAcquire了一次。如果当前线程所在节点tryAcquire成功了,那么执行setHead方法,将当前节点作为head、将当前节点中的thread设置为null、将当前节点的prev设置为null,这保证了数据结构中头结点永远是一个不带Thread的空节点。为什么需要判断当前线程所在节点的前继节点是head节点,就再次的去tryAcquire了一次呢?
情况二:如果当前线程所在节点的前继节点不是head节点,那么它就需要判断自己需不需要进行阻塞了,因为至少到目前为止,它真的没有机会再去获取锁了。当前线程进行阻塞的大前提是,需要寻找一个前继节点的waitStatus为SIGNAL的节点,这是AQS约定的。只有自己节点的前继节点的waitStatus是SIGNAL,我这个节点才可以安心的去阻塞。因为我的前继节点的waitStatus是SIGNAL,就相当于我告诉了我的前继节点,我将要去阻塞了,到时候请唤醒我。请看shouldParkAfterFailedAcquire(Node pred, Node node)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//拿到前驱的状态
if (ws == Node.SIGNAL)
//如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
return true;
if (ws > 0) {
/*
* 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
* 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这里会判断当前节点的前驱节点的状态,如果:
(1). 如果当前节点的前驱节点的waitStatus是SIGNAL,返回true,表示当前节点应当park。
这个时候就会调用parkAndCheckInterrupt()方法:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//调用park()使线程进入waiting状态
return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}
当前线程就会被阻塞住。从这个方法还可以看出,如果这个线程被唤醒了,这个线程自己会返回在它阻塞期间有没有被中断过。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。所以在这里我们可以明白,在acquireQueued(final Node node, int arg)方法的第869行中,如果这个线程被唤醒了,并且曾经在阻塞期间被中断过,就将中断标识符interrupted置为true。接着线程又会进入acquireQueued(final Node node, int arg)的for循环中。如果当前这个被唤醒的线程是正常被唤醒的,那么它的前继节点就应该是head,这个时候当前被唤醒的线程就会执行tryAcquire方法去获取锁。如果假设它获取锁成功了,那么它会把自己设置为head节点,并且把head节点的持有线程设置为null,以保持head节点是dummy节点,接着当前线程就去做自己的业务了。
(2).如果当前节点的前驱节点的waitStatus>0,相当于CANCELLED(因为状态值里面只有CANCELLED是大于0的),那么CANCELLED的节点作废,当前节点不断向前找并重新连接为双向队列,直到找到一个前驱节点waitStats不是CANCELLED的并且最靠近head节点的那一个为止。它的前驱节点不是SIGNAL状态且waitStatus<=0,利用CAS机制把前驱节点的waitStatus更新为SIGNAL状态。在这种情况下parkAndCheckInterrupt返回的是false,也就是说当前节点持有的线程还是不死心,它还需要最后一次tryAcquire,这也是它最后的一次挣扎的机会了。如果这一次失败了,就必须进行阻塞。并取消获取锁,调用cancelAcquire(Node node) 如下:
/**该方法实现某个node取消获取锁。**/
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// 遍历并更新节点前驱,把node的prev指向前部第一个非取消节点。
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 记录pred节点的后继为predNext,后续CAS会用到。
Node predNext = pred.next;
// 直接把当前节点的等待状态置为取消,后继节点即便也在cancel可以跨越node节点。
node.waitStatus = Node.CANCELLED;
/*
* 如果CAS将tail从node置为pred节点了
* 则剩下要做的事情就是尝试用CAS将pred节点的next更新为null以彻底切断pred和node的联系。
* 这样一来就断开了pred与pred的所有后继节点,这些节点由于变得不可达,最终会被回收掉。
* 由于node没有后继节点,所以这种情况到这里整个cancel就算是处理完毕了。
*
* 这里的CAS更新pred的next即使失败了也没关系,说明有其它新入队线程或者其它取消线程更新掉了。
*/
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// 如果node还有后继节点,这种情况要做的事情是把pred和后继非取消节点拼起来。
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
/*
* 如果node的后继节点next非取消状态的话,则用CAS尝试把pred的后继置为node的后继节点
* 这里if条件为false或者CAS失败都没关系,这说明可能有多个线程在取消,总归会有一个能成功的。
*/
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
/*
* 这时说明pred == head或者pred状态取消或者pred.thread == null
* 在这些情况下为了保证队列的活跃性,需要去唤醒一次后继线程。
* 举例来说pred == head完全有可能实际上目前已经没有线程持有锁了,
* 自然就不会有释放锁唤醒后继的动作。如果不唤醒后继,队列就挂掉了。
*
* 这种情况下看似由于没有更新pred的next的操作,队列中可能会留有一大把的取消节点。
* 实际上不要紧,因为后继线程唤醒之后会走一次试获取锁的过程,
* 失败的话会走到shouldParkAfterFailedAcquire的逻辑。
* 那里面的if中有处理前驱节点如果为取消则维护pred/next,踢掉这些取消节点的逻辑。
*/
unparkSuccessor(node);
}
/*
* 取消节点的next之所以设置为自己本身而不是null,
* 是为了方便AQS中Condition部分的isOnSyncQueue方法,
* 判断一个原先属于条件队列的节点是否转移到了同步队列。
*
* 因为同步队列中会用到节点的next域,取消节点的next也有值的话,
* 可以断言next域有值的节点一定在同步队列上。
*
* 在GC层面,和设置为null具有相同的效果。
*/
node.next = node;
}
}
这里再总结一下acquireQueued()函数的具体流程:
1. 结点进入队尾后,检查状态,找到安全休息点;
2. 调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
3. 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。
3.2 响应中断的独占锁获取锁的流程
获取响应中断的独占锁使用acquireInterruptibly()函数,acquireInterruptibly()函数定义如下:
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
tryAcquire方法前面说过了,是子类实现的一个方法,如果tryAcquire返回的是true,即表明当前线程获得了锁,自然也就不需要构建数据结构进行阻塞等待。如果tryAcquire方法返回的是false,那么当前线程没有获得锁,接着执行”doAcquireInterruptibly(int arg)”这句代码。所有的这一切都是基于当前线程没有被interrupted的。
第一:具体分析doAcquireInterruptibly(int arg)流程:
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这个函数的执行逻辑与前面的不响应中断锁的获取的过程中的区别在:
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
如果线程被中断,会抛出异常,做出中断响应。最终会执行cancelAcquire(node);,该方法流程如下:
/** 该方法实现某个node取消获取锁。*/
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// 遍历并更新节点前驱,把node的prev指向前部第一个非取消节点。
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 记录pred节点的后继为predNext,后续CAS会用到。
Node predNext = pred.next;
// 直接把当前节点的等待状态置为取消,后继节点即便也在cancel可以跨越node节点。
node.waitStatus = Node.CANCELLED;
/*
* 如果CAS将tail从node置为pred节点了
* 则剩下要做的事情就是尝试用CAS将pred节点的next更新为null以彻底切断pred和node的联系。
* 这样一来就断开了pred与pred的所有后继节点,这些节点由于变得不可达,最终会被回收掉。
* 由于node没有后继节点,所以这种情况到这里整个cancel就算是处理完毕了。
*
* 这里的CAS更新pred的next即使失败了也没关系,说明有其它新入队线程或者其它取消线程更新掉了。
*/
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// 如果node还有后继节点,这种情况要做的事情是把pred和后继非取消节点拼起来。
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
/*
* 如果node的后继节点next非取消状态的话,则用CAS尝试把pred的后继置为node的后继节点
* 这里if条件为false或者CAS失败都没关系,这说明可能有多个线程在取消,总归会有一个能成功的。
*/
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
/*
* 这时说明pred == head或者pred状态取消或者pred.thread == null
* 在这些情况下为了保证队列的活跃性,需要去唤醒一次后继线程。
* 举例来说pred == head完全有可能实际上目前已经没有线程持有锁了,
* 自然就不会有释放锁唤醒后继的动作。如果不唤醒后继,队列就挂掉了。
*
* 这种情况下看似由于没有更新pred的next的操作,队列中可能会留有一大把的取消节点。
* 实际上不要紧,因为后继线程唤醒之后会走一次试获取锁的过程,
* 失败的话会走到shouldParkAfterFailedAcquire的逻辑。
* 那里面的if中有处理前驱节点如果为取消则维护pred/next,踢掉这些取消节点的逻辑。
*/
unparkSuccessor(node);
}
/*
* 取消节点的next之所以设置为自己本身而不是null,
* 是为了方便AQS中Condition部分的isOnSyncQueue方法,
* 判断一个原先属于条件队列的节点是否转移到了同步队列。
*
* 因为同步队列中会用到节点的next域,取消节点的next也有值的话,
* 可以断言next域有值的节点一定在同步队列上。
*
* 在GC层面,和设置为null具有相同的效果。
*/
node.next = node;
}
}
3.3 不响应中断的共享锁获取锁的流程
不响应中断的共享锁模式的获取锁的的入口函数acquireShared(long arg),其中调用函数以共享方式tryAcquireShared(arg) 尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。如果tryAcquireShared返回的是负数,即表明当前线程获取锁失败,自然也就需要构建数据结构进行阻塞等待,此时需要进入到doAcquireShared方法了。如果tryAcquireShared方法返回的是正数,那么当前线程已经获得了锁,则直接跳过,去执行自己的业务。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
如果tryAcquireShared返回的是负数, 那么流程会走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);
}
}
可以看出,doAcquireShared方法与前面分析的doAcquire方法的区别是在锁资源值大于0的时候,就证明我们现在共享锁的资源充足,可能目前有线程阻塞在队列中,需要去唤醒当前节点的下一个节点,这就是共享锁唤醒的传播性。执行setHeadAndPropagate(Node node, int propagate)
/**
* 这个函数做的事情有两件:
* 1. 在获取共享锁成功后,设置head节点
* 2. 根据调用tryAcquireShared返回的状态以及节点本身的等待状态来判断是否要需要唤醒后继线程。
*/
private void setHeadAndPropagate(Node node, int propagate) {
// 把当前的head封闭在方法栈上,用以下面的条件检查。
Node h = head;
setHead(node);
/*
* propagate是tryAcquireShared的返回值,这是决定是否传播唤醒的依据之一。
* h.waitStatus为SIGNAL或者PROPAGATE时也根据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();
}
}
中间调用doReleaseShared() 这是共享锁中的核心唤醒函数,主要做的事情就是唤醒下一个线程或者设置传播状态。后继线程被唤醒后,会尝试获取共享锁,如果成功之后,则又会调用setHeadAndPropagate,将唤醒传播下去。这个函数的作用是保障在acquire和release存在竞争的情况下,保证队列中处于等待状态的节点能够有办法被唤醒。
private void doReleaseShared() {
/*
* 以下的循环做的事情就是,在队列存在后继线程的情况下,唤醒后继线程;
* 或者由于多线程同时释放共享锁由于处在中间过程,读到head节点等待状态为0的情况下,
* 虽然不能unparkSuccessor,但为了保证唤醒能够正确稳固传递下去,设置节点状态为PROPAGATE。
* 这样的话获取锁的线程在执行setHeadAndPropagate时可以读到PROPAGATE,从而由获取锁的线程去释放后继等待线程。
*/
for (;;) {
Node h = head;
// 如果队列中存在后继线程。
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
}
// 如果h节点的状态为0,需要设置为PROPAGATE用以保证唤醒的传播。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// 检查h是否仍然是head,如果不是的话需要再进行循环。
if (h == head)
break;
}
}
具体唤醒后继线程会调用unparkSuccessor(Node node)
private void unparkSuccessor(Node node) {
//这里,node一般为当前线程所在的结点。
int ws = node.waitStatus;
if (ws < 0)//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//找到下一个需要唤醒的结点s
if (s == null || s.waitStatus > 0) {//如果为空或已取消
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
3.4 响应中断的共享锁获取锁的流程
响应中断的共享锁获取锁的执行逻辑与不响应中断的共享锁获取锁差异在,如果这个线程被唤醒了,并且曾经在阻塞期间被中断过,就直接抛出了中断异常。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();//在阻塞期间被中断过,就直接抛出了中断异常。
}
} finally {
if (failed)
cancelAcquire(node);
}
}
4 基于AQS实现一个不响应中断的独占锁NonReentrantMutex
前面的源码分析中已经分析过,AQS用来实现锁或其他同步组件的基础框架,用户只需要重写它的不同模式获取锁和释放锁方法即可,不响应中断的独占锁只需要重写tryAcquire(int acquire),tryRelease(int releases) 即可:
package concurrent;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class NonReentrantMutex implements Lock, Serializable {
/**自定义同步器**/
public static class Sync extends AbstractQueuedSynchronizer {
/**判断是否锁定状态**/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
/**尝试获取资源,立即返回。成功则返回true,否则false**/
@Override
protected boolean tryAcquire(int acquire) {
assert acquire == 1;
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**尝试释放资源,立即返回。成功则为true,否则false**/
@Override
protected boolean tryRelease(int releases) {
assert releases == 1;
if (getState() == 0)
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
/**同步类的实现都依赖继承于AQS的自定义同步器**/
private final Sync sync = new Sync();
/**lock acquire。两者语义一样:获取资源,即便等待,直到成功才返回**/
public void lock() {
sync.acquire(1);
}
/**响应中断的获取锁**/
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/**unlock<-->release。两者语文一样:释放资源**/
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return null;
}
/**锁是否占有状态**/
public boolean isLock() {
return sync.isHeldExclusively();
}
/**tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false**/
public boolean tryLock() {
return sync.tryAcquire(1);
}
/**尝试获取资源,在unit秒内等待**/
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, 100);
}
}
5 总结
AQS毫无疑问是Doug Lea大师令人叹为观止的作品,它实现精巧、鲁棒、优雅,很好地封装了同步状态的管理、线程的等待与唤醒,足以满足大多数同步工具的需求。
阅读AQS的源码不是一蹴而就就能完全读懂的,阅读源码大致分为三步:
- 读懂大概思路以及一些重要方法之间的调用关系
- 逐行看代码的具体实现,知道每一段代码是干什么的
- 琢磨参悟某一段代码为什么是这么写的,能否换一种写法,能否前后几行代码调换顺序,作者是怎么想的
尽管看懂源码,也可能远远达不到能再造一个能与之媲美的轮子的程度,但是能对同步框架、锁、线程等有更深入的理解,也是很丰硕的收获了。