1、简介
(1)AQS(AbstractQueuedSynchronizer)是实现各种锁的基础,因此必须要非常了解
(2)依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量,事件等)
(3)AQS是Concurrent包的核心,lock就是在AQS的基础上实现的,阻塞队列,线程池,信号量等都离不开AQS的支持。
2、AQS框架设计
它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:
- getState()
- setState()
- compareAndSetState()
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
3、自定义同步器需要自己实现的方法
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
-
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
-
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
-
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
-
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
-
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
以上的获取和释放都是循环自旋CAS设置同步状态变量实现。独占模式下只需要实现tryAcquire(int)和tryRelease(int);而共享模式下只需要实现tryAcquireShare(int)和tryReleaseShare(int);特俗如读写锁,同时包含了独占模式和共享模式。
4、继承关系
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
5、源码详解(!!!)
AQS内部包含了2个内部类(Node和ConditionObject),及一系列方法,其中我们要重点与 获取/释放 同步状态相关的方法。
5.1、Node节点类
static final class Node {
// 共享模式下等待的标记
static final Node SHARED = new Node();
// 独占模式下等待的标记
static final Node EXCLUSIVE = null;
// 线程的等待状态 表示线程已经被取消
static final int CANCELLED = 1;
// 线程的等待状态 表示后继线程需要被唤醒
static final int SIGNAL = -1; //头节点一定是,但其他正常等待的节点也可能是
// 线程的等待状态 表示线程在Condtion上
static final int CONDITION = -2;
// 表示下一个acquireShared需要无条件的传播
static final int PROPAGATE = -3;
/**
* SIGNAL: 当前节点的后继节点处于等待状态时,如果当前节点的同步状态被释放或者取消,
* 必须唤起它的后继节点
*
* CANCELLED: 一个节点由于超时或者中断需要在CLH队列中取消等待状态,被取消的节点不会再次等待
*
* CONDITION: 当前节点在等待队列中,只有当节点的状态设为0的时候该节点才会被转移到同步队列
*
* PROPAGATE: 下一次的共享模式同步状态的获取将会无条件的传播
* waitStatus的初始值时0,使用CAS来修改节点的状态
*/
volatile int waitStatus;
/**
* 当前节点的前驱节点,当前线程依赖它来检查waitStatus,在入队的时候才被分配,
* 并且只在出队的时候才被取消(为了GC),头节点永远不会被取消,一个节点成为头节点
* 仅仅是成功获取到锁的结果,一个被取消的线程永远也不会获取到锁,线程只取消自身,
* 而不涉及其他节点
*/
volatile Node prev;
/**
* 当前节点的后继节点,当前线程释放的才被唤起,在入队时分配,在绕过被取消的前驱节点
* 时调整,在出队列的时候取消(为了GC)
* 如果一个节点的next为空,我们可以从尾部扫描它的prev,双重检查
* 被取消节点的next设置为指向节点本身而不是null,为了isOnSyncQueue更容易操作
*/
volatile Node next;
/**
* 当前节点的线程,初始化后使用,在使用后失效
*/
volatile Thread thread;
/**
* 链接到下一个节点的等待条件,或特殊的值SHARED,因为条件队列只有在独占模式时才能被访问,
* 所以我们只需要一个简单的连接队列在等待的时候保存节点,然后把它们转移到队列中重新获取
* 因为条件只能是独占性的,我们通过使用特殊的值来表示共享模式
*/
Node nextWaiter;
/**
* 如果节点处于共享模式下等待直接返回true
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回当前节点的前驱节点,如果为空,直接抛出空指针异常
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//下面是3个构造方法
Node() { // 用来建立初始化的head 或 SHARED的标记
}
Node(Thread thread, Node mode) { // 指定线程和模式的构造方法
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // 指定线程和节点状态的构造方法
this.waitStatus = waitStatus;
this.thread = thread;
}
}
5.2、ConditionObject内部类
对于ConditionObject内部类,现在不做介绍。这个类将会在后面专门将Condition时做详细介绍。现在只需要知道它是使线程处于等待某个条件的作用即可(await()、signal()、signalAll() )
5.3、AQS的成员变量
//序列化标识(不需要背),只需要知道AQS实现了Serializable接口即可
private static final long serialVersionUID = 7373984972572414691L;
//注意:volatile(transient表示不会序列化)
private transient volatile Node head; //同步队列头节点
private transient volatile Node tail; //同步队列尾节点
private volatile int state; //同步状态
//CAS自选下,时间很短时的时间。不是timeout
static final long spinForTimeoutThreshold = 1000L;
//下面的一些成员以后再补充,现在只需要了解(和JVM、反射有关)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;
static {
try {
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
headOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
tailOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
waitStatusOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("waitStatus"));
nextOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("next"));
} catch (Exception ex) { throw new Error(ex); }
}
同步队列中首节点是获取到锁的节点,它在释放的时候会唤醒后继节点,后继节点获取到锁的时候,会把自己设为首节点。
注意,设置首节点不需要使用CAS,因为在并发环境中只有一个线程都获取到锁,只有获取到锁的线程才能设置首节点。
5.4、独占式
5.4.1、获取同步状态 acquire(int)
通过调用acquire的方法获取同步状态,该方法忽略中断,线程获取同步状态失败后,进入同步队列,在对其进行中断操作后,线程不会从同步队列移除(lock就是调用acqurire()方法实现的)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* 当前线程的自我中断
*/
private static void selfInterrupt() {
Thread.currentThread().interrupt();
}
函数流程如下:
(1)tryAcquire(),获取同步状态,AQS并没有实现这个方法,具体的实现由它的继承类进行重写,比如ReentrantLock的Sync类。如果获取同步状态成功的直接返回true;如果获取同步状态失败的话,进行下面的调用。
(2)addWaiter(),将该线程封装成Node,加入等待队列的尾部,并标记为独占模式
(3)acquireQueued(),使节点自旋方式获取同步状态;获取成功则返回;获取失败,则挂起线程;一直到获取到资源才返回。线程如果在获取同步状态中和同步队列中被中断过,要进行自我中断。在整个等待过程中被中断过,则返回true,否则返回false。
(4)selfInterrupt(),线程进行自我中断,前面的acquireQueued()被中断过返回true,就要进行自我中断
5.4.2、tryAcquire(int)
该方法需要自定义同步器自己来实现,AQS中没有实现。返回值为boolean类型。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现吗?就是这里了!!!AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)!!!至于能不能重入,能不能加塞,那就看具体的自定义同步器怎么去设计了!!!当然,自定义同步器在进行资源访问时要考虑线程安全的影响。
5.4.3、addWaiter(Node mode)
将当前线程构造成节点,加入到同步队列尾部
/**
* 把Node节点添加到同步队列的尾部
*/
private Node addWaiter(Node mode) {
// 以独占模式把当前线程封装成一个Node节点
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速入队,如果失败,再调用enq(node)以死循环方式入队
Node pred = tail; // 当前队列的尾节点赋给pred
if (pred != null) { // 先觉条件 尾节点不为空
node.prev = pred; // 把pred作为node的前继节点
if (compareAndSetTail(pred, node)) { //利用CAS把node作为尾节点
pred.next = node; // 把node作为pred的后继节点
return node; // 直接返回node
}
}
enq(node); // 尾节点为空或者利用CAS把node设为尾节点失败
return node;
}
/**
* 采用自旋的方式把node插入到队列中
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 如果t为空,说明队列为空,必须初始化
// 新建一个节点利用CAS设为头节点,就是这样的形式 head=tail=null
if (compareAndSetHead(new Node()))
tail = head;
} else { // 尾节点不为空的情况
node.prev = t; // 把t设为node的前驱节点
if (compareAndSetTail(t, node)) { // 利用CAS把node节点设为尾节点
t.next = node; // 更改指针 把node作为t的后继节点
return t; // 直接返回t
}
}
}
}
enq方法中采用了非常经典的自旋操作,只有通过CAS把node设为尾节点(volatile变量,写入后对其他线程立马可见)后,当前线程才能退出该方法,否则的话,当前线程不断的尝试,直到能把节点添加到队列中为止,这样就把并行添加变成了串行添加。
5.4.4、acquireQueued(Node, int)
上面一步是将当前线程构造成同步节点,并将节点以循环CAS方式添加到同步队列尾节点。
acquireQueued(Node,int)是是Node循环获取同步状态int,其实质上还是循环调用tryAcquire(int)。通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了。聪明的你立刻应该能想到该线程下一部该干什么了吧:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。没错,就是这样!是不是跟医院排队拿号有点相似~~acquireQueued()就是干这件事:在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回。
/*
* 此主要是通过自旋方式获取同步状态
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //标记是否成功拿去到资源
try {
boolean interrupted = false; // 标记等待过程中是否被中断过
//循环CAS获取资源
for (;;) {
final Node p = node.predecessor(); // 获取该节点的前驱节点p
// 如果前驱节点是头节点并且能获取到同步状态(因为FIFO,只有头节点的后继节点才有资格获取)
if (p == head && tryAcquire(arg)) {
//不用担心多线程发生竞争,因为同一时刻,获取到同步状态的线程只有一个,因此不用CAS
setHead(node); // 把当前节点设为头节点
p.next = null; // 把p的next设为null(相当于原来头节点出同步队列),便于GC
failed = false; // 标志--表示成功获取同步状态,默认是true,表示失败
return interrupted; // (返回点)返回该线程在获取到同步状态的过程中有没有被中断过
}
if (shouldParkAfterFailedAcquire(p, node) && // 用于判断是否挂起当前线程
parkAndCheckInterrupt())
interrupted = true; //如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
}
} finally {
if (failed) //如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
cancelAcquire(node);
}
}
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。
下面看下shouldParkAfterFailedAcquire(p, node)方法
/**
* 如果线程获取同步状态失败就要检查它的节点status,要保证prev = node.prev
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 获取当前节点的前驱节点的waitStatus
if (ws == Node.SIGNAL)
//如果前驱节点的ws = singal,表示前驱节点释放后会唤起当前线程,可以安全的挂起当前线程
return true; // 能够挂起当前线程直接返回true
if (ws > 0) { //就是Cancelled
//前驱节点的ws > 0,说明ws = Cancelled,表示前驱线程被取消,从前驱节点继续往前遍历,直到找到第一个前驱节点的ws <= 0 为止
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else { //0 | PPROPAGATE
//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
就是看前驱节点的状态:
- signal,表示前驱节点释放同步状态后,会唤醒当前节点(因此当前线程可以安全挂起)
- cancelled,表示前驱节点取消,应该继续向前找,知道第一个ws<=0(也就是不是取消状态的节点)
- 初始状态(0)或者是propagate(传播状态),表示不能挂起当前线程,需要将前驱节点设置成signal状态才行
从这里可以看出signal并不一定是头节点的状态,其他正常等待的节点都可能是这个状态(当然头节点一定是signal)
下面看下parkAndCheckInterrupt()方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //调用LockSupport工具挂起当前线程
//Thread.interrupted()会清除当前线程的中断标记位
return Thread.interrupted(); //返回当前线程是否被中断过
}
如果上一步的方法中前驱节点是signal,则表示可以让当前线程挂起,因此调用parkAndCheckInterrupt()方法。被唤醒的可能有2种:
- 被unpark()
- 被interrupt()
5.4.5、cancelAcquire(node)
获取同步状态失败(超时,或者中断),取消节点。
private void cancelAcquire(Node node) {
// 当前节点不存在的话直接忽略
if (node == null)
return;
node.thread = null; // 把当前节点的线程设为null
// 获取当前节点的前驱pred
Node pred = node.prev;
while (pred.waitStatus > 0) // 如果prde的ws > 0,直接跳过pred继续往前遍历,直到pred的ws <= 0
node.prev = pred = pred.prev;
// 获取pred的后继predNext
Node predNext = pred.next;
// 把node节点的ws设为CANCELLED
node.waitStatus = Node.CANCELLED;
// 如果node是尾节点,利用CAS把pred设为尾节点,predNext为null
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else { //node不是尾节点
// pred不是头结点 && pred的线程不为空 && pred.ws = singal
// 利用CAS把node的next设为pred的next节点
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next); //就是将中间的cancelled节点出队
} else { // node是头结点,唤起它的后继节点
unparkSuccessor(node);
}
node.next = node; // node指向自己,便于GC
}
}
主要做了6件事
- 找到node节点的第一个不是cancelled的前面的节点pre
- 设置node节点的状态为cancelled
- 如果node是尾节点,则将pre设置为尾节点,preNext=null
- 如果pre不是头节点,且pre为signal,则连接pre和node.next,就是将中间全部的cancelled节点出队
- 如果node是头节点,则唤醒node的后继节点
- node.next=node,方面node回收
下面介绍下unparkSuccessor(node)方法
/**
* 如果node存在唤醒它的后继节点
*/
private void unparkSuccessor(Node node) {
//获取node的ws,如果ws<0,使用CAS把node的ws设为0,表示释放同步状态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* 获取node的后继节点s,根据条件s = null 或者 s.ws > 0,从同步队列的尾部开始遍历,
* 直到找到距node最近的满足ws <= 0的节点t,把t赋给s,唤醒s节点的线程
* 如果s不为null && s的ws <= 0,直接唤醒s的线程
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); //唤醒节点s对应的线程
}
就是先把node的状态CAS成0,释放同步状态
然后取得node的后继节点next。如果node=null或者node.waitStatus=cancelled,则找到距离node最近的节点s(从tail往前找),满足s.waitStatus<=0,则将s赋值给next。降下来唤醒next节点对应的线程即可。
5.4.6、获取同步状态总结(独占式)
5.4.7、释放同步状态(独占式)release(int)
unlock()即使调用这个。先释放同步状态,再唤醒后继节点(包含原来头节点出队列)。
/**
* 以独占模式释放同步状态,当前线程释放同步状态的时候,会唤醒同步队列上的后继节点
* 释放成功后之后直接返回true
*/
public final boolean release(int arg) {
if (tryRelease(arg)) { //其实现一般为循环CAS实现,很大可能是返回true
Node h = head; //获取头节点
if (h != null && h.waitStatus != 0) //头节点不为空,且状态不为0(没有释放)
unparkSuccessor(h); //唤醒头节点的next节点(其中就包含了将头节点状态设置为0,以及对next节点为空|cancelled等情况的处理)
return true;
}
return false;
}
它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点!!
5.4.8、tryRelease(int)
具体自己实现
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
5.4.9、响应式中断获取同步状态 acquireInterruptibly(int arg)
/**
* 当前线程被中断后,直接抛出异常,否则的话,再次调用tryAcquire方法获取同步状态
*/
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException(); //如果线程被中断了,抛出异常
if (!tryAcquire(arg)) //获取同步状态失败
doAcquireInterruptibly(arg); //中断式获取同步状态
}
5.4.10、doAcquireInterruptibly(int arg)
这个方法和acquire()方法很相似,只不过线程在被中断后直接抛出异常,综合了acquire() + addWaiter() + acquireQueued()
/**
* 以独占模式获取同步状态,线程被中断直接抛出异常
*/
private void doAcquireInterruptibly(int arg) throws InterruptedException {
//也是在尝试tryAcquire()失败之后,将当前线程创建节点,加入到同步队列的尾节点
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);
}
}
5.4.11、指定时间内获取同步状态 tryAcquireNanos(int arg, long nanosTimeout)
响应中断,且有时间限制
/**
* 以独占模式获取同步状态,线程被中断,直接抛出异常,如果在指定时间内没有获取到同步状态,
* 直接返回false,表现获取同步状态失败.
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//如果获取状态成功,则不会执行do....;如果获取状态失败,则执行do...
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
5.4.12、doAcquireNanos(int arg, long nanosTimeout)
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L) //时间参数异常
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE); //将当前线程装成Node,添加到同步队列的尾节点
boolean failed = true;
try {
//就是如果前驱是头节点,则尝试获取;如果获取失败,或者前驱不是头节点,则进入带时间线程unpark()
for (;;) { //死循环获取同步状态
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
etHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime(); //计算剩余时间
if (nanosTimeout <= 0L)
return false; //超时
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold) //剩余时间大于CAS自旋界限时间,如果剩余时间小于...,则自旋CAS
LockSupport.parkNanos(this, nanosTimeout); //超时悬挂线程
if (Thread.interrupted()) //影响中断
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在第一次尝试获取同步状态失败之后,进入这个方法。先将当前线程装成节点,加入同步队列尾部。然后进入死循环。如果前驱是头节点,则尝试获取;如果获取失败,或者前驱不是头节点,则进入带时间线程unpark()。过程中带时间限制,响应中断。
5.5、共享式
最大的特点就是:同一时刻允许多个线程获取到同步状态
5.5.1、获取同步状态 acquireShared(int arg)
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) //尝试获取同步状态
doAcquireShared(arg); //进入等待队列,直到获取资源
}
acquireShared方法就是先调用tryAcquireShare来获取同步状态,如果获取失败,则进入等待队列,循环获取。整个过程忽略中断。与独占式的区别是,独占式的返回值boolean,而共享式是int,可能取值如下:
- 0:获取成功,没有剩余资源
- n:获取成功,还有剩余资源n供其他线程获取
- <0:获取失败
5.5.2、doAcquireShared(int)
将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回
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) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
int r = tryAcquireShared(arg);//尝试获取资源
if (r >= 0) {//成功
setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // help GC
if (interrupted)//如果等待过程中被打断过,此时将中断补上。
selfInterrupt();
failed = false;
return;
}
}
//判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
和独占式的acquireQueued基本一样,区别在于:
- ryAcquireShare和tryAcquire的返回值类型不一样
- setHead(node)和 setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程。因为独占式一个获取同步状态之后,其他线程不能获取,因为唤醒后继节点在头节点释放同步的时候。而共享式可以多个线程同时获取同步状态,因此,一个线程获取之后,还要循环其他线程来获取同步状态。
- selfInterrupt()放到doAcquireShared()里了,而独占模式是放到acquireQueued()之外
5.5.3、setHeadAndPropagate(Node, int)
设置node为头节点,同时唤醒后继节点(因为同一时刻可以有多个线程获取同步状态)
private void setHeadAndPropagate(Node node, int propagate) { //propagate是tryAcquireShare的返回值
Node h = head; // 保存当前的头节点
setHead(node); // 把当前节点设为头节点
/*
* 这里有三种情况执行唤醒操作:1.propagate > 0(还有剩余资源),代表后继节点需要被唤醒
* 2. h节点的ws < 0或者 h=null
* 3. 新的头结点为空 或者 新的头结点的ws < 0
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next; // 找到当前节点的后继节点s
if (s == null || s.isShared()) // s=null 或者 s是共享模式,调用doReleaseShared方法唤醒后继线程
doReleaseShared();
}
}
在设置头节点成功之后,由于是共享模式,如果剩余资源数>0 | 头节点=null或者状态为cancelled,则应该唤醒后继节点
5.5.4、doReleaseShared()
唤醒后继节点
private void doReleaseShared() {
/*
* 注意,这里的头结点已经是上面新设定的头结点了,从这里可以看出,如果propagate=0,
* 不会进入doReleaseShared方法里面,那就有共享式变成了独占式.
*/
for (;;) { // 这里一个死循环直到满足条件h=head才能跳出
Node h = head;
if (h != null && h != tail) { // 前提条件-当前的头结点不为null && h不是尾节点
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { // 如果当前头结点的ws=signal,利用CAS把h的ws设为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h); // 唤醒头结点的后继节点
} // 如果h的ws=0,就把h的ws设为PROPAGATE,表示可以向后传播唤醒
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head) // 如果头结点没有发生改变,表示设置完成,可以退出循环
break; // 如果头结点发生了变化,可能被唤醒的其他节点重新设置了头结点
} // 这样头结点发生了改变,要进行重试,保证可以传播唤醒信号
}
5.5.5、releaseShared(int arg)
释放资源,唤醒后继节点
//释放同步状态
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 尝试释放资源
doReleaseShared(); //释放资源成功,则唤醒后继节点
return true;
}
return false;
}
5.5.6、tryReleaseShared(int arg)
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
AQS中没有具体实现这个方法,在自定义同步器中去实现。
5.5.7、共享式中断式获取 acquireSharedInterruptibly(int arg)
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException(); //响应中断
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg); //中断式获取
}
5.5.8、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);
}
}
和doAcquireShared(int)基本类似,只是添加了响应中断的代码
6、总结
独占式
共享式
之后说下关于函数的开头:
- try开头的AQS没有具体实现,除了 tryAcquireNanos(int arg, long nanosTimeout) 和 tryAcquireSharedNanos(int arg, long nanosTimeout)
- 获取同步状态变量那步一定是 tryAcquire 和 tryAcquireShare
- 响应中断的是 acquireSharedInterruptibly 和 acquireInterruptibly,他们分别调用 doAcquireSharedInterruptibly 和 doAcquireInterruptibly
- 超时获取的是 tryAcquireNanos 和 tryAcquireSharedNanos,他们分别调用 doAcquireNanos 和 doAcquireSharedNanos
- 所以以do开头的都是响应中断|超时获取里面调用的
- 在顶层调用的是 acquire、acquireShare、响应中断、超时获取 那几个