AbstractQueuedSynchronizer(AQS)是JDK中实现并发编程的核心,平时我们工作中经常用到的ReentrantLock,CountDownLatch等都是基于它来实现的。
AQS类中维护了一个双向链表(FIFO队列), 如下图所示:
队列中的每个元素都用一个Node表示,我们可以看到,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;
}
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;
}
}
成员变量:
//head:等待队列头部,延迟初始化,直到调用enq才真正初始化;
private transient volatile Node head;
//tail:等待队列尾部,延迟初始化,直到调用enq才真正初始化;
private transient volatile Node tail;
//state:AQS状态位,通过try*方法维护;
private volatile int state;
Node是构成同步队列的基础,看一下Node的结构:
同步队列中首节点是获取到锁的节点,它在释放的时候会唤醒后继节点,后继节点获取到锁的时候,会把自己设为首节点。
注意,设置首节点不需要使用CAS,因为在并发环境中只有一个线程都获取到锁,只有获取到锁的线程才能设置首节点。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作。
其主要逻辑是:
- 首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态。
- 如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部。
- 最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。
- 如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
下面来看一下同步器将节点加入到同步队列的过程:
从上图中我们可以看到,接入新的节点进入队列时,最后一个节点会将它的“next”节点指向新的节点,新的节点的“prev”会指向老的尾部节点,同时,同步器的“tail”节点会更新,指向新加入的节点。
我们再来看一下JDK中的源代码实现:
上述代码通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加。
试想一下:如果使用一个普通的LinkedList来维护节点之间的关系,那么当一个线程获取了同步状态,而其他多个线程由于调用tryAcquire(int arg)方法获取同步状态失败而并发地被添加到LinkedList时,LinkedList将难以保证Node的正确添加,最终的结果可能是节点的数量有偏差,而且顺序也是混乱的。
在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。可以看出,enq(final Node node)方法将并发添加节点的请求通过CAS变得“串行化”了。当第一次构建队列的时候,此时头结点与尾节点全部都是空的,这时,首先会新增一个空的头结点,然后将新的节点放置到尾部,这里的空的头结点非常精妙,后面我们再来说这里的设计。
节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)。代码如下:
/*
* 此主要是通过自旋方式获取同步状态
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false; // 默认线程没有被中断过
for (;;) {
final Node p = node.predecessor(); // 获取该节点的前驱节点p
if (p == head && tryAcquire(arg)) { // 如果p是头节点并且能获取到同步状态
setHead(node); // 把当前节点设为头节点
p.next = null; // 把p的next设为null,便于GC
failed = false; // 标志--表示成功获取同步状态,默认是true,表示失败
return interrupted; // 返回该线程在获取到同步状态的过程中有没有被中断过
}
if (shouldParkAfterFailedAcquire(p, node) && // 用于判断是否挂起当前线程
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) // 如果fail为true,直接移除当前节点
cancelAcquire(node);
}
}
这个方法比较复杂,里面包含了很多其他的方法,我们先看获取当前节点的前驱节点,如果前驱节点是头节点,有两种情况,一种是默认空的头节点,说明此时是同步队列中的第一个线程去尝试获取同步状态,另一种是获取到同步状态的节点,然后再一次调用子类重写的tryAcquire方法去获取同步状态,如果成功获取同步状态,则把当前节点设为头节点。如果当前节点的前驱节点不是头节点或者没有获取到同步状态的话,就要调用shouldParkAfterFailedAcquire方法判断是否挂起当前线程。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取pred前置节点的等待状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
/* 前置节点状态是signal,那当前节点可以安全阻塞,因为前置节点承诺执行完之后会通知唤醒当前
* 节点
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
// 前置节点如果已经被取消了,则一直往前遍历直到前置节点不是取消状态,与此同时会修改链表关系
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 前置节点是0或者propagate状态,这里通过CAS把前置节点状态改成signal
// 这里不返回true让当前节点阻塞,而是返回false,目的是让调用者再check一下当前线程是否能
// 成功获取锁,失败的话再阻塞,这里说实话我也不是特别理解这么做的原因
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这段代码用来检测是否挂起当先线程,分三种情况,第一种情况是前驱节点的 ws = singal,表示前驱节点释放同步状态的时候会唤醒当前节点,可以安全挂起当前线程;第二种情况是前驱节点被取消,那就从前驱节点继续往前遍历,直到往前找到第一个ws <= 0 的节点;第三种是前驱节点的 ws = 0,表示前驱节点获取到同步状态,当前线程不能挂起,应该尝试去获取同步状态,前驱节点的同步状态的释放正好可以让当前节点进行获取,所以使用CAS把前驱节点的ws设为singal,另外如果 ws =PROPAGATE,说明正以共享模式进行传播,也需要使用CAS把ws设为singal.
在shouldParkAfterFailedAcquire返回true的情况下,继续看parkAndCheckInterrupted方法
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程,监事是当前sync对象
LockSupport.park(this);
// 阻塞返回后,返回当前线程是否被中断
return Thread.interrupted();
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
// 设置当前线程的监视器blocker
setBlocker(t, blocker);
// 这里调用了native方法到JVM级别的阻塞机制阻塞当前线程
UNSAFE.park(false, 0L);
// 阻塞结束后把blocker置空
setBlocker(t, null);
}
调用LockSupport的park方法挂起当前线程,返回该线程是否被中断过,如果被中断过,直接设置interrupted = true.
如果获取同步状态失败,采用cancelAcquire方法取消当前节点
/**
* 取消当前节点
*/
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的
node.prev = pred = pred.prev; // ws <= 0
// 获取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 {
// 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);
} else { // node是头结点,唤起它的后继节点
unparkSuccessor(node);
}
node.next = node; // node指向自己,便于GC
}
}
分三种情况进行考虑:
1. node本身就是尾节点,直接把node的prev设为尾节点
2. node的prev不是头结点,直接把prev和node的next进行连接
3. node的prev是头结点,使用unparkSuccessor唤醒后继节点
这就是整个acquireQueued的流程,如果执行完acquireQueued方法返回线程被中断过,那线程最后要进行自我中断一下
/**
* 当前线程的自我中断
*/
private static void selfInterrupt() {
Thread.currentThread().interrupt();
}
问题:在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,这是为什么?原因有两个,如下:
第一,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
第二,维护同步队列的FIFO原则。该方法中,节点自旋获取同步状态的行为如下图所示
由于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱是否是头节点,如果是则尝试获取同步状态。节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放规则符合FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头节点的线程由于中断而被唤醒)。
节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)。
上面说了很多,想必大家可能有点晕,下面我用一张流程图来简化说明一下同步器获取同步状态的流程:
当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,如果对于锁这种并发组件而言,代表着当前线程获取了锁。
当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。
通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。
源码如下:
上面就是释放同步资源的操作,我们来一步一步的分析一下,首先,会通过模板方法调用子类的tryRelease(),如果释放成功,获取当前头结点,如果头结点不为空,同时头结点的等待状态不等于0,则执行unparkSuccessor()方法,唤醒等待的队列中的下一个节点的线程。
/**
* 如果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);
}
在unparkSuccessor()方法中,首先先获取头结点的等待状态,如果等待状态为-1,则将状态置为0。
接下来,找到下一个需要唤醒的结点s,如果它为空或已取消,则从队列中去寻找最前边的那个未放弃的节点。
最后,执行唤醒的操作,通过LockSupport提供的工具类。下一个等待节点的线程被唤醒后,它在自旋tryAcquire()方法会返回true,则表示自己拿到资源,将前一个头结点踢出队列,将自己设置为头结点。
还记得我们之前提到的,当第一次构建队列的时候,此时头结点与尾节点全部都是空的,这时,首先会新增一个空的头结点,其实这里的设计非常精妙。
我在看源码的时候,是很好奇为何要初始化的时候设置一个空的头结点,其主要的原因是:如果没有一个空的头结点,在acquireQueued()方法中的自旋就会出现问题,因为自旋是判断的自己的前一个节点是否为头节点,如果第一次构建队列,就把当前等待节点放置在头结点,它就没有前置节点了,它的自旋条件永远无法成立。因此,空的头结点的创建是非常必要的。
好啦,我们刚刚分析了独占式同步状态获取与释放以及同步队列的原理,我们来总结一下这块:
- 线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试;
- 线程获取锁是一个自旋的过程,当且仅当当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞;
- 释放锁的时候会唤醒后继节点。
总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。
我们在前面提到过,Lock锁的实现与synchronized相比,更加的灵活,可以响应中断以及超时时间设置等特性,而Lock的这些特性的实现也是基于AQS的acquireInterruptibly()方法实现的,我们现在来看一下源码的实现
/**
* 当前线程被中断后,直接抛出异常,否则的话,再次调用tryAcquire方法获取同步状态
*/
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(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);
}
}
从上面的代码中可以看到,基本上实现与acquire()一致,唯一的区别是当parkAndCheckInterrupt返回true时,即线程阻塞时该线程被中断,代码抛出被中断异常。
AQS中的tryAcquireNanos()方法可以设置一个超时时间,该方法会在三种情况下才会返回:
我们来看一下其源码实现:
/**
* 以独占模式获取同步状态,线程被中断,直接抛出异常,如果在指定时间内没有获取到同步状态,
* 直接返回false,表现获取同步状态失败.
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, 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);
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 true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我们用一张流程图拉来描述一下其流程:
程序逻辑同独占锁可响应中断式获取基本一致,唯一的不同在于获取锁失败后,对超时时间的处理上,
在第1步会先计算出按照现在时间和超时时间计算出理论上的截止时间,比如当前时间是8h10min,超时时间是10min,那么根据deadline = System.nanoTime() + nanosTimeout计算出刚好达到超时时间时的系统时间就是8h 10min+10min = 8h 20min。
然后根据deadline - System.nanoTime()就可以判断是否已经超时了,比如,当前系统时间是8h 30min很明显已经超过了理论上的系统时间8h 20min,deadline - System.nanoTime()计算出来就是一个负数,自然而然会在3.2步中的If判断之间返回false。
如果还没有超时即3.2步中的if判断为true时就会继续执行3.3步通过LockSupport.parkNanos使得当前线程阻塞,同时在3.4步增加了对中断的检测,若检测出被中断直接抛出被中断异常。
上面我们了解到了独占式获取同步状态的实现,AQS中还有共享式获取同步状态的实现。独占式和共享式的最大不同就是在同一时刻能否有多个线程获取同步状态,通过调用acquireShared方法获取同步状态。我们来看下源码实现:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
/**
* 以共享非中断获取同步状态
*/
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);
}
}
tryAcquireShared是留给子类去重写的,如果tryAcquireShared方法返回值<0,说明获取同步状态失败,执行doAcquireShared方法,在doAcquireShared再次调用tryAcquireShared方法,判断其返回值,若返回值<0,获取同步状态失败,需要进入同步队列进行等待,若返回值 >= 0,如果返回值=0,说明当前线程获取同步状态成功,其他线程无法获取,也就不需要唤醒它的后继节点进行传播.如果返回值>0,此时当前线程获取同步状态后要唤醒它的后继节点,让其他线程也尝试去获取同步状态。
独占式获取同步状态之后,直接返回中断状态,结束流程,共享式则调用setHeadAndPropagate方法传播唤醒的动作。
private void setHeadAndPropagate(Node node, int propagate) {
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();
}
}
此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式!doReleaseShared()我们留着下面的releaseShared()里来讲。
上面已经把acquireShared()说完了,下面就来讲讲它的反操作releaseShared()吧。此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。下面是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()的返回值。*/
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; // 如果头结点发生了变化,可能被唤醒的其他节点重新设置了头结点
} // 这样头结点发生了改变,要进行重试,保证可以传播唤醒信号
}
本文,我们深入了解了AQS的源码实现,了解了AQS的队列实现结构,学习了独占式获取同步状态与共享式获取同步状态的实现机制,了解AQS的实现机制对于我们后续学习ReentrantLock、ReentrantReadWriteLock等Lock的实现非常至关重要。