若是你想深刻研究Java并发的话,那么AQS必定是绕不开的一块知识点,Java并发包不少的同步工具类底层都是基于AQS来实现的,好比咱们工做中常常用的Lock工具ReentrantLock、栅栏CountDownLatch、信号量Semaphore等,并且关于AQS的知识点也是面试中常常考察的内容,因此,不管是为了更好的使用仍是为了应付面试,深刻学习AQS都颇有必要。node
学习AQS以前,咱们有必要了解一个知识点,就是AQS底层中大量使用的CAS,关于CAS,你们应该都不陌生,若是还有哪位同窗不清楚的话,能够看看我以前的文章《面试必问系列:悲观锁和乐观锁的那些事儿》,这里很少复述,哈哈,给本身旧文章加了阅读量面试
本文主角正式登场。segmentfault
AQS,全名AbstractQueuedSynchronizer,是一个抽象类的队列式同步器,它的内部经过维护一个状态volatile int state(共享资源),一个FIFO线程等待队列来实现同步功能。数据结构
state用关键字volatile修饰,表明着该共享资源的状态一更改就能被全部线程可见,而AQS的加锁方式本质上就是多个线程在竞争state,当state为0时表明线程能够竞争锁,不为0时表明当前对象锁已经被占有,其余线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中,这些线程会被UNSAFE.park()操做挂起,等待其余获取锁的线程释放锁才可以被唤醒。多线程
而这个等待队列其实就至关于一个CLH队列,用一张原理图来表示大体以下:并发
AQS支持两种资源分享的方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。函数
自定义的同步器继承AQS后,只须要实现共享资源state的获取和释放方式便可,其余如线程队列的维护(如获取资源失败入队/唤醒出队等)等操做,AQS在顶层已经实现了,工具
AQS代码内部提供了一系列操做锁和线程队列的方法,主要操做锁的方法包含如下几个:oop
像ReentrantLock就是实现了自定义的tryAcquire-tryRelease,从而操做state的值来实现同步效果。源码分析
除此以外,AQS内部还定义了一个静态类Node,表示CLH队列的每个结点,该结点的做用是对每个等待获取资源作了封装,包含了须要同步的线程自己、线程等待状态.....
咱们能够看下该类的一些重点变量:
static final class Node {
/** 表示共享模式下等待的Node */ static final Node SHARED = new Node(); /** 表示独占模式下等待的mode */ static final Node EXCLUSIVE = null; /** 下面几个为waitStatus的具体值 */ static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; volatile int waitStatus; /** 表示前面的结点 */ volatile Node prev; /** 表示后面的结点 */ volatile Node next; /**当前结点装载的线程,初始化时被建立,使用后会置空*/ volatile Thread thread; /**连接到下一个节点的等待条件,用到Condition的时候会使用到*/ Node nextWaiter; }
代码里面定义了一个表示当前Node结点等待状态的字段waitStatus,该字段的取值包含了CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、0,这五个值表明了不一样的特定场景:
也就是说,当waitStatus为负值表示结点处于有效等待状态,为正值的时候表示结点已被取消。
在AQS内部中还维护了两个Node对象head和tail,一开始默认都为null
private transient volatile Node head; private transient volatile Node tail;
讲完了AQS的一些基础定义,咱们就能够开始学习同步的具体运行机制了,为了更好的演示,咱们用ReentrantLock做为使用入口,一步步跟进源码探究AQS底层是如何运做的,这里说明一下,由于ReentrantLock底层调用的AQS是独占模式,因此下文讲解的AQS源码也是针对独占模式的操做
好了,热身正式结束,来吧。
咱们都知道,ReentrantLock的加锁和解锁方法分别为lock()和unLock(),咱们先来看获取锁的方法,
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
逻辑很简单,线程进来后直接利用CAS尝试抢占锁,若是抢占成功state值回被改成1,且设置对象独占锁线程为当前线程,不然就调用acquire(1)再次尝试获取锁。
咱们假定有两个线程A和B同时竞争锁,A进来先抢占到锁,此时的AQS模型图就相似这样:
继续走下面的方法,
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
acquire包含了几个函数的调用,
tryAcquire:尝试直接获取锁,若是成功就直接返回;
addWaiter:将该线程加入等待队列FIFO的尾部,并标记为独占模式;
acquireQueued:线程阻塞在等待队列中获取锁,一直获取到资源后才返回。若是在整个等待过程当中被中断过,则返回true,不然返回false。
selfInterrupt:自我中断,就是既拿不到锁,又在等待时被中断了,线程就会进行自我中断selfInterrupt(),将中断补上。
咱们一个个来看源码,并结合上面的两个线程来作场景分析。
不用多说,就是为了再次尝试获取锁
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
当线程B进来后,nonfairTryAcquire方法首先会获取state的值,若是为0,则正常获取该锁,不为0的话判断是不是当前线程占用了,是的话就累加state的值,这里的累加也是为了配合释放锁时候的次数,从而实现可重入锁的效果。
固然,由于以前锁已经被线程A占领了,因此这时候tryAcquire会返回false,继续下面的流程。
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
这段代码首先会建立一个和当前线程绑定的Node节点,Node为双向链表。此时等待队列中的tail指针为空,直接调用enq(node)方法将当前线程加入等待队列尾部,而后返回当前结点的前驱结点,
private Node enq(final Node node) { // CAS"自旋",直到成功加入队尾 for (;;) { Node t = tail; if (t == null) { // 队列为空,初始化一个Node结点做为Head结点,并将tail结点也指向它 if (compareAndSetHead(new Node())) tail = head; } else { // 把当前结点插入队列尾部 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
第一遍循环时,tail指针为空,初始化一个Node结点,并把head和tail结点都指向它,而后第二次循环进来以后,tail结点不为空了,就将当前的结点加入到tail结点后面,也就是这样:
todo 若是此时有另外一个线程C进来的话,发现锁已经被A拿走了,而后队列里已经有了线程B,那么线程C就只能乖乖排到线程B的后面去,
接着解读方法,经过tryAcquire()和addWaiter(),咱们的线程仍是没有拿到资源,而且还被排到了队列的尾部,若是让你来设计的话,这个时候你会怎么处理线程呢?其实答案也很简单,能作的事无非两个:
一、循环让线程再抢资源。但仔细一推敲就知道不合理,由于若是有多个线程都参与的话,你抢我也抢只会下降系统性能
二、进入等待状态休息,直到其余线程完全释放资源后唤醒本身,本身再拿到资源
毫无疑问,选择2更加靠谱,acquireQueued方法作的也是这样的处理:
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { // 标记是否会被中断 boolean interrupted = false; // CAS自旋 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) // 获取锁失败,则将此线程对应的node的waitStatus改成CANCEL cancelAcquire(node); } } private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) // 前驱结点等待状态为"SIGNAL",那么本身就能够安心等待被唤醒了 return true; if (ws > 0) { /* * 前驱结点被取消了,经过循环一直往前找,直到找到等待状态有效的结点(等待状态值小于等于0) , * 而后排在他们的后边,至于那些被当前Node强制"靠后"的结点,由于已经被取消了,也没有引用链, * 就等着被GC了 */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // 若是前驱正常,那就把前驱的状态设置成SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
acquireQueued方法的流程是这样的:
一、CAS自旋,先判断当前传入的Node的前结点是否为head结点,是的话就尝试获取锁,获取锁成功的话就把当前结点置为head,以前的head置为null(方便GC),而后返回
二、若是前驱结点不是head或者加锁失败的话,就调用shouldParkAfterFailedAcquire,将前驱节点的waitStatus变为了SIGNAL=-1,最后执行parkAndChecknIterrupt方法,调用LockSupport.park()挂起当前线程,parkAndCheckInterrupt在挂起线程后会判断线程是否被中断,若是被中断的话,就会从新跑acquireQueued方法的CAS自旋操做,直到获取资源。
ps:LockSupport.park方法会让当前线程进入waitting状态,在这种状态下,线程被唤醒的状况有两种,一是被unpark(),二是被interrupt(),因此,若是是第二种状况的话,须要返回被中断的标志,而后在acquire顶层方法的窗口那里自我中断补上
此时,由于线程A还未释放锁,因此线程B状态都是被挂起的,
到这里,加锁的流程就分析完了,其实总体来讲也并不复杂,并且当你理解了独占模式加锁的过程,后面释放锁和共享模式的运行机制也没什么难懂的了,因此整个加锁的过程仍是有必要多消化下的,也是AQS的重中之重。
为了方便大家更加清晰理解,我加多一张流程图吧(这个做者也太暖了吧,哈哈)
说完了加锁,咱们来看看释放锁是怎么作的,AQS中释放锁的方法是release(),当调用该方法时会释放指定量的资源 (也就是锁) ,若是完全释放了(即state=0),它会唤醒等待队列里的其余线程来获取资源。
仍是一步步看源码吧,
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
代码上能够看出,核心的逻辑都在tryRelease方法中,该方法的做用是释放资源,AQS里该方法没有具体的实现,须要由自定义的同步器去实现,咱们看下ReentrantLock代码中对应方法的源码:
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
tryRelease方法会减去state对应的值,若是state为0,也就是已经完全释放资源,就返回true,而且把独占的线程置为null,不然返回false。
此时AQS中的数据就会变成这样:
彻底释放资源后,当前线程要作的就是唤醒CLH队列中第一个在等待资源的线程,也就是head结点后面的线程,此时调用的方法是unparkSuccessor(),
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) //将head结点的状态置为0 compareAndSetWaitStatus(node, ws, 0); //找到下一个须要唤醒的结点s Node s = node.next; //若是为空或已取消 if (s == null || s.waitStatus > 0) { s = null; // 从后向前,直到找到等待状态小于0的结点,前面说了,结点waitStatus小于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);//唤醒 }
方法的逻辑很简单,就是先将head的结点状态置为0,避免下面找结点的时候再找到head,而后找到队列中最前面的有效结点,而后唤醒,咱们假设这个时候线程A已经释放锁,那么此时队列中排最前边竞争锁的线程B就会被唤醒,
而后被唤醒的线程B就会尝试用CAS获取锁,回到acquireQueued方法的逻辑,
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; }
当线程B获取锁以后,会把当前结点赋值给head,而后原先的前驱结点 (也就是原来的head结点) 去掉引用链,方便回收,这样一来,线程B获取锁的整个过程就完成了,此时AQS的数据就会变成这样:
到这里,咱们已经分析完了AQS独占模式下加锁和释放锁的过程,也就是tryAccquire->tryRelease这一链条的逻辑,除此以外,AQS中还支持共享模式的同步,这种模式下关于锁的操做核心其实就是tryAcquireShared->tryReleaseShared这两个方法,咱们能够简单看下
AQS中,共享模式获取锁的顶层入口方法是acquireShared,该方法会获取指定数量的资源,成功的话就直接返回,失败的话就进入等待队列,直到获取资源,
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
该方法里包含了两个方法的调用,
tryAcquireShared:尝试获取必定资源的锁,返回的值表明获取锁的状态。
doAcquireShared:进入等待队列,并循环尝试获取锁,直到成功。
tryAcquireShared在AQS里没有实现,一样由自定义的同步器去完成具体的逻辑,像一些较为常见的并发工具Semaphore、CountDownLatch里就有对该方法的自定义实现,虽然实现的逻辑不一样,但方法的做用是同样的,就是获取必定资源的资源,而后根据返回值判断是否还有剩余资源,从而决定下一步的操做。
返回值有三种定义:
当返回值小于0时,证实这次获取必定数量的锁失败了,而后就会走doAcquireShared方法
此方法的做用是将当前线程加入等待队列尾部休息,直到其余线程释放资源唤醒本身,本身成功拿到相应量的资源后才返回,这是它的源码:
private void doAcquireShared(int arg) { // 加入队列尾部 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; // CAS自旋 for (;;) { final Node p = node.predecessor(); // 判断前驱结点是不是head if (p == head) { // 尝试获取必定数量的锁 int r = tryAcquireShared(arg); if (r >= 0) { // 获取锁成功,并且还有剩余资源,就设置当前结点为head,并继续唤醒下一个线程 setHeadAndPropagate(node, r); // 让前驱结点去掉引用链,方便被GC p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } // 跟独占模式同样,改前驱结点waitStatus为-1,而且当前线程挂起,等待被唤醒 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // head指向本身 setHead(node); // 若是还有剩余量,继续唤醒下一个邻居线程 if (propagate > 0 || h == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } }
看到这里,你会不会一点熟悉的感受,这个方法的逻辑怎么跟上面那个acquireQueued() 那么相似啊?对的,其实两个流程并无太大的差异。只是doAcquireShared()比起独占模式下的获取锁上多了一步唤醒后继线程的操做,当获取完必定的资源后,发现还有剩余的资源,就继续唤醒下一个邻居线程,这才符合"共享"的思想嘛。
这里咱们能够提出一个疑问,共享模式下,当前线程释放了必定数量的资源,但这部分资源知足不了下一个等待结点的须要的话,那么会怎么样?
按照正常的思惟,共享模式是能够多个线程同时执行的才对,因此,多个线程的状况下,若是老大释放完资源,但这部分资源知足不了老二,但能知足老三,那么老三就能够拿到资源。可事实是,从源码设计中能够看出,若是真的发生了这种状况,老三是拿不到资源的,由于等待队列是按顺序排列的,老二的资源需求量大,会把后面量小的老三以及老4、老五等都给卡住。从这一个角度来看,虽然AQS严格保证了顺序,但也下降了并发能力
接着往下说吧,唤醒下一个邻居线程的逻辑在doReleaseShared()中,咱们放到下面的释放锁来解析。
共享模式释放锁的顶层方法是releaseShared,它会释放指定量的资源,若是成功释放且容许唤醒等待线程,它会唤醒等待队列里的其余线程来获取资源。下面是releaseShared()的源码:
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
该方法一样包含两部分的逻辑:
tryReleaseShared:释放资源。
doAcquireShared:唤醒后继结点。
跟tryAcquireShared方法同样,tryReleaseShared在AQS中没有具体的实现,由子同步器本身去定义,但功能都同样,就是释放必定数量的资源。
释放完资源后,线程不会立刻就收工,而是唤醒等待队列里最前排的等待结点。
唤醒后继结点的工做在doReleaseShared()方法中完成,咱们能够看下它的源码:
private void doReleaseShared() { for (;;) { // 获取等待队列中的head结点 Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; // head结点waitStatus = -1,唤醒下一个结点对应的线程 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; } }
代码没什么特别的,就是若是等待队列head结点的waitStatus为-1的话,就直接唤醒后继结点,唤醒的方法unparkSuccessor()在上面已经讲过了,这里也不必再复述。
总的来看,AQS共享模式的运做流程和独占模式很类似,只要掌握了独占模式的流程运转,共享模式什么的不就那样吗,没难度。这也是我为何共享模式讲解中不画流程图的缘由,不必嘛。
介绍完了AQS的核心功能,咱们再扩展一个知识点,在AQS中,除了提供独占/共享模式的加锁/解锁功能,它还对外提供了关于Condition的一些操做方法。
Condition是个接口,在jdk1.5版本后设计的,基本的方法就是await()和signal()方法,功能大概就对应Object的wait()和notify(),Condition必需要配合锁一块儿使用,由于对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,所以Condition通常都是做为Lock的内部实现 ,AQS中就定义了一个类ConditionObject来实现了这个接口,
那么它应该怎么用呢?咱们能够简单写个demo来看下效果
public class ConditionDemo { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); Thread tA = new Thread(() -> { lock.lock(); try { System.out.println("线程A加锁成功"); System.out.println("线程A执行await被挂起"); condition.await(); System.out.println("线程A被唤醒成功"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.out.println("线程A释放锁成功"); } }); Thread tB = new Thread(() -> { lock.lock(); try { System.out.println("线程B加锁成功"); condition.signal(); System.out.println("线程B唤醒线程A"); } finally { lock.unlock(); System.out.println("线程B释放锁成功"); } }); tA.start(); tB.start(); } }
执行main函数后结果输出为:
线程A加锁成功
线程A执行await被挂起
线程B加锁成功
线程B唤醒线程A
线程B释放锁成功
线程A被唤醒成功
线程A释放锁成功
代码执行的结果很容易理解,线程A先获取锁,而后调用await()方法挂起当前线程并释放锁,线程B这时候拿到锁,而后调用signal唤醒线程A。
毫无疑问,这两个方法让线程的状态发生了变化,咱们仔细来研究一下,
翻看AQS的源码,咱们会发现Condition中定义了两个属性firstWaiter和lastWaiter,前面说了,AQS中包含了一个FIFO的CLH等待队列,每一个Conditon对象就包含这样一个等待队列,而这两个属性分别表示的是等待队列中的首尾结点,
/** First node of condition queue. */ private transient Node firstWaiter; /** Last node of condition queue. */ private transient Node lastWaiter;
注意:Condition当中的等待队列和AQS主体的同步等待队列是分开的,两个队列虽然结构体相同,可是做用域是分开的
先看await()的源码:
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); // 将当前线程加入到等待队列中 Node node = addConditionWaiter(); // 彻底释放占有的资源,并返回资源数 int savedState = fullyRelease(node); int interruptMode = 0; // 循环判断当前结点是否是在Condition的队列中,是的话挂起 while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
当一个线程调用Condition.await()方法,将会以当前线程构造结点,这个结点的waitStatus赋值为Node.CONDITION,也就是-2,并将结点从尾部加入等待队列,而后尾部结点就会指向这个新增的结点,
private Node addConditionWaiter() { Node t = lastWaiter; // If lastWaiter is cancelled, clean out. if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; }
咱们依然用上面的demo来演示,此时,线程A获取锁并调用Condition.await()方法后,AQS内部的数据结构会变成这样:
在Condition队列中插入对应的结点后,线程A会释放所持有的资源,走到while循环那层逻辑,
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
isOnSyncQueue方法的会判断当前的线程节点是否是在同步队列中,这个时候此结点还在Condition队列中,因此该方法返回false,这样的话循环会一直持续下去,线程被挂起,等待被唤醒,此时,线程A的流程暂时中止了。
当线程A调用await()方法挂起的时候,线程B获取到了线程A释放的资源,而后执行signal()方法:
public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); }
先判断当前线程是否为获取锁的线程,若是不是则直接抛出异常。 接着调用doSignal()方法来唤醒线程。
private void doSignal(Node first) { // 循环,从队列一直日后找不为空的首结点 do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null); } final boolean transferForSignal(Node node) { // CAS循环,将结点的waitStatus改成0 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; // 上面已经分析过,此方法会把当前结点加入到等待队列中,并返回前驱结点 Node p = enq(node); int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
从doSignal的代码中能够看出,这时候程序寻找的是Condition等待队列中首结点firstWaiter的结点,此时该结点指向的是线程A的结点,因此以后的流程做用的都是线程A的结点。
这里分析下transferForSignal方法,先经过CAS自旋将结点waitStatus改成0,而后就把结点放入到同步队列 (此队列不是Condition的等待队列) 中,而后再用CAS将同步队列中该结点的前驱结点waitStatus改成Node.SIGNAL,也就是-1,此时AQS的数据结构大概以下 (额.....少画了个箭头,你们就当head结点是线程A结点的前驱结点就好):
回到await()方法,当线程A的结点被加入同步队列中时,isOnSyncQueue()会返回true,跳出循环,
while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode);
接着执行acquireQueued()方法,这里就不用多说了吧,尝试从新获取锁,若是获取锁失败继续会被挂起,直到另外线程释放锁才被唤醒。
因此,当线程B释放完锁后,线程A被唤醒,继续尝试获取锁,至此流程结束。
对于这整个通讯过程,咱们能够画一张流程图展现下:
说完了Condition的使用和底层运行机制,咱们再来总结下它跟普通 wait/notify 的比较,通常这也是问的比较多的,Condition大概有如下两点优点:
对AQS的源码分析到这里就所有结束了,虽然还有不少知识点没讲解,好比公平锁/非公平锁下AQS是怎么做用的,篇幅所限,部分知识点没有扩展还请见谅,尽管如此,若是您能看完文章的话,相信对AQS也算是有足够的了解了。
回顾本篇文章,咱们不难发现,不管是独占仍是共享模式,或者结合是Condition工具使用,AQS本质上的同步功能都是经过对锁和队列中结点的操做来实现的,从设计上讲,AQS的组成结构并不算复杂,底层的运起色制也不会很绕,因此,你们若是看源码的时候以为有些困难的话也不用灰心,多看几遍,顺便画个图之类的,理清下流程仍是没什么问题的。
固然,本身看得懂是一回事,写出来让别人看懂又是另外一回事了,就像这篇文章,我花了好长的时间来准备,又是画图又是理流程的,期间还参考了很多网上大神的博文,肝了几天才算是成文了。虽然我知道本文不算什么高质文,但我也算是费尽心力了,写技术文真是挺累的,你们看的以为不错的话还请帮忙转发下或点个赞吧!这也是对我最好的鼓励了
做者:鄙人薛某,一个不拘于技术的互联网人,技术三流,吹水一流,想看更多精彩文章能够关注个人公众号哦~~~