若是你想深刻研究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的組成結構並不算複雜,底層的運起色制也不會很繞,因此,你們若是看源碼的時候以爲有些困難的話也不用灰心,多看幾遍,順便畫個圖之類的,理清下流程仍是沒什麼問題的。
固然,本身看得懂是一回事,寫出來讓別人看懂又是另外一回事了,就像這篇文章,我花了好長的時間來準備,又是畫圖又是理流程的,期間還參考了很多網上大神的博文,肝了幾天纔算是成文了。雖然我知道本文不算什麼高質文,但我也算是費盡心力了,寫技術文真是挺累的,你們看的以爲不錯的話還請幫忙轉發下或點個贊吧!這也是對我最好的鼓勵了
做者:鄙人薛某,一個不拘於技術的互聯網人,技術三流,吹水一流,想看更多精彩文章能夠關注個人公衆號哦~~~