1.概念
AQS全称AbstractQueuedSynchronize(java.util.concurrent.locks),即抽象的队列式的同步器。
AQS定义了一套多线程访问共享资源的同步器框架,许多锁和同步器实现都依赖于它,如常用的而ReentrantLock、Semaphore、LockSupport等java.util.concurrent包下大量的锁和同步器。【AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架】
private static final class Sync extends AbstractQueuedSynchronizer {
*****
}
AQS 定义了两种资源共享方式:
Exclusive:独占,只有一个线程能执行,如ReentrantLockShare:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
**特殊:**一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
ReentrantReadWriteLock【读写锁】
它表示两个锁,一种是读操作相关的锁,称为共享锁;一种是写相关的锁,称为排他锁。
不同的自定义的同步器争用共享资源的方式也不同。
2.AQS内部结构及原理
加锁会导致阻塞、有阻塞就需要排队,实现排队必然需要队列。如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。
- 内置的**
CLH(FIFO)队列**的变种来完成资源获取线程的排队工作- 将每条将要去抢占资源的线程封装成一个**
Node节点**来实现锁的分配- 有一个int类变量表示持有锁的状态(private volatile int
state),通过**CAS原子的修改共享标志位**、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。
CLH:Craig、Landin and Hagersten 队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO
Node
static final class Node {
/** 节点在共享模式下等待 */
static final Node SHARED = new Node();
/** 节点在独占模式下等待 */
static final Node EXCLUSIVE = null;
/**
* 表示当前结点已取消调度
* 当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
*/
static final int CANCELLED = 1;
/**
* 表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL
*/
static final int SIGNAL = -1;
/**
* 表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等 * 待获取同步锁。
*/
static final int CONDITION = -2;
/**
* 共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点
*/
static final int PROPAGATE = -3;
/**
* 节点在队列种的等待状态
*/
volatile int waitStatus;
/**
* 前驱节点
*/
volatile Node prev;
/**
* 后继节点
*/
volatile Node next;
/**
* 线程对象
*/
volatile Thread thread;
/**
*
*/
Node nextWaiter;
}
3.成员变量
/**
* 头节点
*/
private transient volatile Node head;
/**
* 尾节点
*/
private transient volatile Node tail;
/**
* 标识同步状态
*/
private volatile int state;
1.state
用于判断共享资源是否正在本占用的标志位,volatile保证线程之间的可见性。
线程获取锁的两种模式:
- 独占:一个线程以独占模式获取锁时,其他线程必须等待。仅有一个线程可执行
- 共享:一个线程以共享模式获取锁时,其他也想以共享模式获取锁的线程,也能够增强锁,从而一起访问共享资源。可以多个线同时执行。
在共享模式下,可能存在多个线程正在共享资源,所以state需要表示线程占用的个数,所以使用了int类型,而非Boolean类型
以**
CountDownLatch**以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作
2.head、tail
头节点、尾节点
一个线程在当前时刻没有获取到共享资源,可以进行排队。 而排队的队列数据结构则是一个FIFO(先进先出的双向链表),head和tail 则表示该链表的头和尾。
4.主要方法
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
1.acquire
**获取资源:**此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
该方法用public final 修饰,表示不允许所有的子类进行修改,直接调用父类方法即可。说明该方法一定可以获得锁。
执行过程:
tryAcquire(arg)尝试获取锁(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待)
tryAcquire(arg)尝试获取锁成功,acquire()方法直接结束
tryAcquire(arg)尝试获取锁失败,需要执行addWaiter()方法,该线程加入等待队列的尾部,并标记为独占模式。接着执行acquireQueued(Node node, int arg)方法。
acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。且整个过程忽略中断的影响。
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上
tryAcquire
尝试获取资源
// 参数arg 表示对state的修改值,返回值Boolean类型,表示是否获取到锁。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
该方法protected修饰,且仅一行报错代码。需要其子类进行重写。
这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。
该上层调用开放了空间。
AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现
- 尝试获得锁,获取锁或者未获得锁后,进行相应的业务处理。
若业务一定需要获得锁,则下面的acquire()方法可以保证线程一定可以获得锁。
addWaiter
将当前线程封装成一个Node节点,加入等待队列。放回当前节点。
private Node addWaiter(Node mode) {
//1. 将当前线程封装成Node节点 mode有两种:EXCLUSIVE(独占)和SHARED(共享)
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure(先尝试快速入队,如果失败进行完整入队)
//2.将新建的节点插入到队尾
//2.1 获取尾节点
Node pred = tail;
if (pred != null) {
// 2.2 将尾节点作为将新的Node节点 的前驱节点
node.prev = pred;
// 2.3 在尾节点不为空的情况下,通过CAS 将当前节点置为尾节点
if (compareAndSetTail(pred, node)) {
// 2.4 将之前的尾节点(倒数第二个)的后继节点置为新的Node节点(2.3CAS为新的尾节点)
pred.next = node;
return node;
}
}
// 3. 当前尾节点为空 或者 第一次CAS修改尾节点失败时,将执行下面的完整入队方法。
enq(node);
return node;
}
// 2.3 在尾节点不为空的情况下,通过CAS 将当前节点置为尾节点 if (compareAndSetTail(pred, node)) { // 2.4 将之前的尾节点(倒数第二个)的后继节点置为新的Node节点(2.3CAS为新的尾节点) pred.next = node; return node; } compareAndSetTail()方法虽然是一个原子操作,但是整个if代码块并不是一个原子操作。
当2.4正在修改之前尾节点的后继节点时,可能其他线程正在修改尾节点信息。而此时即便尾节点发生了变动,对此操作也不会出现线程安全问题。
完整的入队方法enq()👇👇👇👇👇
private Node enq(final Node node) {
// 自旋,直到成功加入队尾
for (;;) {
Node t = tail;
// // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
if (t == null) { // Must initialize
// 由此可以看出Head节点其实是一个傀儡节点,不包含数据,Head节点并不是当前需要获取锁的节点,
// 只是一个占位的摆设,第二个节点才是需要获取锁的节点
// 所以在AQS 中多出出现判断一个节点的前驱节点是否为Head节点的逻辑
if (compareAndSetHead(new Node()))
tail = head;
//正常流程,放入队尾
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
通过自旋,初始化队列,将当前节点插入到队列
acquireQueued
进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源
final boolean acquireQueued(final Node node, int arg) {
// 标记是否成功拿到资源
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 如果前驱是head,即该结点已成老二。(Head节点作为一个傀儡节点,不需要获取资源)那么便有资格去尝试获取资源
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC 【之前的Head节点出队】
failed = false; // 成功获取资源
return interrupted; //返回等待过程中是否被中断过
}
// 当前节点不是头节点 或者 Head节点尝试获取锁失败
// shouldParkAfterFailedAcquire() 判断当前节点是否需要挂起,若需要挂起,调用parkAndCheckInterrupt()进行park()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; // 如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
}
} finally {
if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
cancelAcquire(node);
}
}
变量failed初始值为true,只有在return之前改为false,而finally{}代码块中,有个判断failed状态的操作,会执行cancelAcquire()方法。说明acquireQueued()方法在正常执行且返回时,failed为false,只有执行过程抛出异常,才会执行finally代码块中的cancelAcquire()方法。
cancelAcquire() 将Node节点的waitStatus状态置为CANCELLED,以及其他的一些清理工作
方法主体,通过自旋操作,如果当前节点的前节点时Head节点,并且当前线程尝试获得锁成功。当第二个节点获取到锁后,会变为头节点(上面已经分析得出头节点只是一个傀儡节点,不用来获取锁)
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true;shouldParkAfterFailedAcquire()
是否需要挂起当前线程,通过前置节点的waitStatusprivate static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; // 如果当前节点的前置节点的waitStatus为SIGNAL ,那么前置节点也在等待获取锁 // 所以当前节点可以直接返回true,进行挂起休息 if (ws == Node.SIGNAL) return true; // 如果当前节点的前置节点的waitStatus大于0,说明其状态为CANCELLED // 可以将其从队列中删除,如此循环,直至当前节点的前置节点的waitStatus小于等于0 // ⚠⚠⚠ 那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被GC回收! if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; // 如果当前节点的前置节点的waitStatus是其他状态 // 既然当前节点已经加入,那么其前驱节点就应该做好准备等待锁 // 所以通过CAS将前驱节点的waitStatus置为SIGNAL // (如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!) } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }private final boolean parkAndCheckInterrupt() { // 挂起线程 LockSupport.park(this); //调用park()使线程进入waiting状态 return Thread.interrupted(); // 如果被唤醒,查看自己是不是被中断的 }下面的两种情况返回false,如果shouldParkAfterFailedAcquire()方法返回true,代表当前节点需要被挂起。
这就保证了只有Head的后继节点一个节点一直进行CAS获取锁的操作,这样就能最大限度的避免无用的自旋消耗CPU。在shouldParkAfterFailedAcquire()方法返回true时,则执行parkAndCheckInterrupt()方法中的挂起操作。
其中LockSupport.park()方法,是通过调用JVM的native方法
interrupted是返回当前线程的中断标识位,并将其复位为false。
acquireQueued方法总结:
- 结点进入队尾后,检查状态,找到安全休息点;
- 调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
- 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。
总结
acquire()方法总结:
- 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
- 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
线程进行了入队等待,那么何时会被唤醒呢?
一个线程使用完共享资源,并且在释放锁时,需要唤醒其他等待锁的线程
2.release
释放锁:此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义
public final boolean release(int arg) {
// 1.尝试释放锁成功
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒等待队列中指定的Node节点(正常情况下,Head节点的后继节点)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease
尝试释放锁
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
可以看出tryRelease()方法和tryAcquire() 一样,都被protected修饰,且仅一行报错代码,都需要其子类进行重写。
跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,**release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!**所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。
unparkSuccessor
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
// 在此,Head节点的使命已经完成,此时Head只是一个占位的傀儡节点,将其waitStatus置为0这个默认值,才不会影响其他函数的判断
compareAndSetWaitStatus(node, ws, 0);
/*
* unpark 的线程保留在后继节点中,通常只是下一个节点。
* 但如果被取消或明显为空,则从尾部向后遍历以找到实际的未取消后继者。
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾节点开始向前搜索,找到除了Head节点外,waitStatus小于0,且最考前的节点
// ⚠⚠⚠⚠⚠ 从这里可以看出,<=0的结点,都是还有效的结点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒指定线程
LockSupport.unpark(s.thread);
}
unparkSuccessor(Node node) 方法 传进来Head节点,该方法是为了唤醒Head节点后面的Node节点,使获取锁。
但如果Head节点的后继节点为空,则需从尾部先前编辑,找到未取消的节点,使其获得锁
为何要倒叙搜索?
- 如果Head节点的后继节点为null,则不可以正序进行搜索。
还有原因嘛?
最后当release()方法执行完成,线程释放锁之后,之前head节点后的线程会通过字段获得锁,如此轮询
总结
- release()方法中的最后逻辑->unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用
T来表示吧。- 此时,再和acquireQueued()联系起来,
T被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然T已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,T也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后T把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了
3.acquireShared
**获取资源:**此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
acquireShared流程:
- tryAcquireShared()尝试获取资源,成功则直接返回;
- 失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。
tryAcquireShared
尝试获取锁(共享模式情况下)
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:
- 负值代表获取失败;
- 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(); //前驱
//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
if (p == head) {
int r = tryAcquireShared(arg); //尝试获取资源
if (r >= 0) { //获取资源成功
setHeadAndPropagate(node, r); //将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // help GC
if (interrupted) //如果等待过程中被打断过,此时将中断补上。
selfInterrupt();
failed = false;
return;
}
}
// shouldParkAfterFailedAcquire() 判断当前节点是否需要挂起,若需要挂起,调用parkAndCheckInterrupt()进行park()
// 进入waiting状态,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
跟独占模式比,还有一点需要注意的是,这里只有线程是head.next时(“老二”),才会去尝试获取资源,有剩余的话还会唤醒之后的队友。
那么问题就来了,假如老大用完后释放了5个资源,而老二需要6个,老三需要1个,老四需要2个。老大先唤醒老二,老二一看资源不够,他是把资源让给老三呢,还是不让?答案是否定的!老二会继续park()等待其他线程释放资源,也更不会去唤醒老三和老四了。独占模式,同一时刻只有一个线程去执行,这样做未尝不可;但共享模式下,多个线程是可以同时执行的,现在因为老二的资源需求量大,而把后面量小的老三和老四也都卡住了。
当然,这并不是问题,只是AQS保证严格按照入队顺序唤醒罢了(保证公平,但降低了并发)
setHeadAndPropagate
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below(记录下旧头以供检查)
setHead(node); //head指向自己
// 如果还有剩余量,继续唤醒下一个邻居线程
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()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式!
总结:
执行过程:
- tryAcquireShared()尝试获取资源,成功则直接返回;
- 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。
其实跟acquire()的流程大同小异,只不过多了个**
自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)**
4.releaseShared
**释放掉资源后,唤醒后继节点。**此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源
public final boolean releaseShared(int arg) {
// 尝试释放资源
if (tryReleaseShared(arg)) {
// 唤醒后继结点
doReleaseShared();
return true;
}
return false;
}
跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。
doReleaseShared
唤醒后继节点
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
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) // head发生变化,释放资源结束 // loop if head changed
break;
}
}
总结
独占和共享两种模式下获取-释放资源(acquire-release、acquireShared-releaseShared)的源码,相信大家都有一定认识了。值得注意的是,acquire()和acquireShared()两种方法下,线程在等待队列中都是忽略中断的。AQS也支持响应中断的,acquireInterruptibly()/acquireSharedInterruptibly()即是,相应的源码跟acquire()和acquireShared()差不多.
问题:
1.parkAndCheckInterrupt()方法中为何需要return Thread.interrupted();
传递中断标志
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }在LockSupport.park(this);对此线程进行挂起时,在此期间,有可能在AQS以外的其他操作想要中转这个线程, 调用了这个线程的interrupted()方法。(当线程处于等待状态时,调用interrupted()中断会抛出interruptedException)
而加入一个线程时通过wait、sleep,那么再调用interrupted()中断会抛出interruptedException,而使用LockSupport.park(this)进行挂起,即使再调用interrupted()方法,也不会抛出异常,此时如果其他线程在某个地方调用了该线程的interrupted()方法,只会改变这个线程对象内部的一个中断的状态值,所以这里需要一个变量值记录下来。
如果外部调用了该线程的interrupted()方法,那么该线程被唤醒时,即parkAndCheckInterrupt()中的LockSupport.park(this);,需要return Thread.interrupted();
当线程处于等待队列时,无法响应外部的中断请求,只有当这个线程拿到锁之后,然后再进行中断
2.park()
park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。
需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。
3.独占和共享selfInterrupt()的位置
共享:补中断的selfInterrupt()放到doAcquireShared()里
独占:放到acquireQueued()之外
其他:
1.设计模式:模板方法
AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
- 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
- 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。