Java并發(fā)編程之JUC并發(fā)核心AQS同步隊列原理剖析
一、AQS介紹
隊列同步器AbstractQueuedSynchronizer(簡稱AQS),AQS定義了一套多線程訪問共享資源的同步器框架,是用來構(gòu)建鎖或者其他同步組件的基礎框架,是一個依賴狀態(tài)(state)的同步器。Java并發(fā)編程的核心在java.util.concurrent(簡稱juc)包,而juc包的大部分工具都是以AQS為基礎進行構(gòu)建的,例如Semaphore、ReentranLock、CountDownLatch、CyclicBarrier等,它的作者是鼎鼎大名的Doug Lea。
AQS具備特性
- 阻塞等待隊列
- 共享/獨占
- 公平/非公平
- 可重入
- 允許中斷
它維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。state的訪問方式有三種:
- getState() 獲取state
- setState() 設置state
- compareAndSetState() 通過CAS的方式設置state值
AQS有兩種資源共享方式:Exclusive(獨占式) 和 Share(共享式)。所謂獨占式是指依據(jù)AQS中的state控制狀態(tài),只有一個線程能夠進行工作(其它參與調(diào)度的線程會進入阻塞狀態(tài),如ReentrantLock);共享式是指,依據(jù)AQS中的state控制狀態(tài),可以有多個滿足條件的線程同時執(zhí)行(如Semaphore/CountDownLatch)?!?/p>
AQS定義兩種隊列
- 同步等待隊列(基于雙向鏈表實現(xiàn))
- 條件等待隊列(基于單向鏈表實現(xiàn))
不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現(xiàn)時只需要實現(xiàn)共享資源state的獲取與釋放方式即可,至于具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經(jīng)在頂層實現(xiàn)好了。一般通過定義內(nèi)部類Sync繼承AQS將同步器所有調(diào)用都映射到Sync對應的方法。
自定義同步器實現(xiàn)時主要實現(xiàn)以下幾種方法:
- isHeldExclusively():該線程是否正在獨占資源,如果返回true,則表示當前線程正在獨占資源。只有用到condition才需要去實現(xiàn)它。
- tryAcquire(int):獨占方式。嘗試獲取資源,成功則返回true,失敗則返回false。
- tryRelease(int):獨占方式。嘗試釋放資源,成功則返回true,失敗則返回false。
- tryAcquireShared(int):共享方式。嘗試獲取資源。負數(shù)表示失?。?表示成功,但沒有剩余可用資源;正數(shù)表示成功,且有剩余資源。
- tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放后允許喚醒后續(xù)等待結(jié)點返回true,否則返回false。
實現(xiàn)自定義同步組件時,將會調(diào)用同步器提供的模板方法,這些(部分)模板方法與描述如下。同步器提供的模板方法基本上分為3類:獨占式獲取與釋放同步狀態(tài)、共享式獲取與釋放同步狀態(tài)和查詢同步隊列中的等待線程情況。自定義同步組件將使用同步器提供的模板方法來實現(xiàn)自己的同步語義。
- acquire(int arg):獨占式獲取同步狀態(tài),如果當前線程獲取同步狀態(tài)成功,則由該方法返回,否則,將會進入同步隊列等待,該方法將會調(diào)用可重寫的tryAcquire(int arg)方法;
- acquireInterruptibly(int arg):與acquire(int arg)相同,但是該方法響應中斷,當前線程為獲取到同步狀態(tài)而進入到同步隊列中,如果當前線程被中斷,則該方法會拋出InterruptedException異常并返回;
- tryAcquireNanos(int arg,long nanos):超時獲取同步狀態(tài),如果當前線程在nanos時間內(nèi)沒有獲取到同步狀態(tài),那么將會返回false,已經(jīng)獲取則返回true;
- acquireShared(int arg):共享式獲取同步狀態(tài),如果當前線程未獲取到同步狀態(tài),將會進入同步隊列等待,與獨占式的主要區(qū)別是在同一時刻可以有多個線程獲取到同步狀態(tài);
- acquireSharedInterruptibly(int arg):共享式獲取同步狀態(tài),響應中斷;
- tryAcquireSharedNanos(int arg, long nanosTimeout):共享式獲取同步狀態(tài),增加超時限制;
- release(int arg):獨占式釋放同步狀態(tài),該方法會在釋放同步狀態(tài)之后,將同步隊列中第一個節(jié)點包含的線程喚醒;
- releaseShared(int arg):共享式釋放同步狀態(tài);
二、AQS中的隊列
1、同步等待隊列
AQS當中的同步等待隊列也稱CLH隊列,CLH隊列是Craig、Landin、Hagersten三人發(fā)明的一種基于雙向鏈表數(shù)據(jù)結(jié)構(gòu)的隊列,是FIFO先入先出線程等待隊列,Java中的CLH隊列是原CLH隊列的一個變種,線程由原自旋機制改為阻塞機制。
這種結(jié)構(gòu)的特點是每個數(shù)據(jù)結(jié)構(gòu)都有兩個指針,分別指向直接的后繼節(jié)點和直接前驅(qū)節(jié)點。所以雙向鏈表可以從任意一個節(jié)點開始很方便的訪問前驅(qū)和后繼。每個 Node 其實是由線程封裝,當線程爭搶鎖失敗后會封裝成 Node 加入到 ASQ 隊列中去;當獲取鎖的線程釋放鎖以后,會從隊列中喚醒一個阻塞的節(jié)點線程 。
2、條件等待隊列
條件等待隊列是單向鏈表實現(xiàn)的,此時Node(下面會介紹)中pre和next都為null。Condition是一個多線程間協(xié)調(diào)通信的工具類,使得某個、或者某些線程一起等待某個條件(Condition),只有當該條件具備時,這些等待線程才會被喚醒,從而重新爭奪鎖。
3、AQS隊列節(jié)點Node
同步隊列中的節(jié)點(Node)用來保存獲取同步狀態(tài)失敗的線程引用、等待狀態(tài)以及前驅(qū)和后繼節(jié)點,節(jié)點的屬性類型與名稱等,Node類基本屬性定義如下所示,它是在AbstractQueuedSynchronizer中的一個內(nèi)部類。
注意:如果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; static final int CONDITION = -2; static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread; Node nextWaiter; }
Node pre:前驅(qū)節(jié)點,當前節(jié)點加入到同步隊列中被設置(尾部添加)
Node next:后繼節(jié)點
Thread thread:節(jié)點同步狀態(tài)的線程
Node nextWaiter:等待隊列中的后繼節(jié)點,如果當前節(jié)點是共享的,那么這個字段是一個SHARED常量,也就是說節(jié)點類型(獨占和共享)和等待隊列中的后繼節(jié)點共用同一個字段
int waitStatus:等待狀態(tài),標記當前節(jié)點的信號量狀態(tài) (1,0,-1,-2,-3)5種狀態(tài),使用CAS更改狀態(tài),volatile保證線程可見性,高并發(fā)場景下,即被一個線程修改后,狀態(tài)會立馬讓其他線程可見,五種狀態(tài)分別為:
- CANCELLED,值為1,在同步隊列中等待的線程等待超時或者被中斷,需要從同步隊列中取消等待,節(jié)點進入該狀態(tài)后將不會變化
- SIGNAL,值為-1,后繼節(jié)點的線程處于等待狀態(tài),而當前的節(jié)點如果釋放了同步狀態(tài)或者被取消,將會通知后繼節(jié)點,使后繼節(jié)點的線程得以運行。
- CONDITION,值為-2,節(jié)點在等待隊列中,節(jié)點的線程等待在Condition上,當其他線程對Condition調(diào)用了signal()方法后,該節(jié)點會從等待隊列中轉(zhuǎn)移到同步隊列中,加入到同步狀態(tài)的獲取中
- PROPAGATE ,值為-3,表示下一次共享式同步狀態(tài)獲取將會被無條件地傳播下去
- INITIAL,值為0,初始狀態(tài)
三、同步隊列源碼分析
1、同步隊列分析
同步器擁有首節(jié)點(head)和尾節(jié)點(tail),沒有成功獲取同步狀態(tài)的線程將會成為節(jié)點加入該隊列的尾部,同步隊列的基本結(jié)構(gòu)如圖所示。
同步器包含了兩個節(jié)點類型的引用,一個指向頭節(jié)點,而另一個指向尾節(jié)點。同步器提供了一個基于CAS的設置尾節(jié)點的方法:compareAndSetTail(Node expect,Node update),它需要傳遞當前線程“認為”的尾節(jié)點和當前節(jié)點,只有設置成功后,當前節(jié)點才正式與之前的尾節(jié)點建立關聯(lián)。 涉及兩個變化:
- 1. 新的線程封裝成 Node 節(jié)點追加到同步隊列中,設置 prev 節(jié)點以及修改當前節(jié)點的前置節(jié)點的 next 節(jié)點指向自己
- 2. 通過 CAS 講 tail 重新指向新的尾部節(jié)點
head節(jié)點表示獲取鎖成功的節(jié)點,當頭結(jié)點在釋放同步狀態(tài)時,會喚醒后繼節(jié)點,如果后繼節(jié)點獲得鎖成功,會把自己設置為頭結(jié)點,節(jié)點的變化過程如下
同步隊列遵循FIFO,首節(jié)點是獲取同步狀態(tài)成功的節(jié)點,首節(jié)點的線程在釋放同步狀態(tài)時,將會喚醒后繼節(jié)點,而后繼節(jié)點將會在獲取同步狀態(tài)成功時將自己設置為首節(jié)點,該過程如下圖所示。涉及兩個變化:
- 1. 修改 head 節(jié)點指向下一個獲得鎖的節(jié)點
- 2. 新的獲得鎖的節(jié)點,將 prev 的指針指向 null
設置首節(jié)點是通過獲取同步狀態(tài)成功的線程來完成的,由于只有一個線程能夠成功獲取到同步狀態(tài),因此設置頭節(jié)點的方法并不需要使用CAS來保證,它只需要將首節(jié)
點設置成為原首節(jié)點的后繼節(jié)點并斷開原首節(jié)點的next引用即可。
2、同步隊列——獨占模式源碼分析
acquire方法(獨占獲取)源碼分析
通過調(diào)用同步器的acquire(int arg)方法可以獲取同步狀態(tài),該方法對中斷不敏感,也就是由于線程獲取同步狀態(tài)失敗后進入同步隊列中,后續(xù)對線程進行中斷操作時,線程不會從同步隊列中移出。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
上面方法中首先調(diào)用自定義同步器實現(xiàn)的tryAcquire(int arg)方法(重寫該方法),該方法保證線程安全的獲取同步狀態(tài),如果同步狀態(tài)獲取失敗,則構(gòu)造同步節(jié)點(獨占式Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態(tài))并通過addWaiter(Node node)方法將該節(jié)點加入到同步隊列的尾部,最后調(diào)用acquireQueued(Node node,int arg)方法,使得該節(jié)點以“死循環(huán)”的方式獲取同步狀態(tài)。如果獲取不到則阻塞節(jié)點中的線程,而被阻塞線程的喚醒主要依靠前驅(qū)節(jié)點的出隊或阻塞線程被中斷來實現(xiàn)。
下面看下addWaiter方法的實現(xiàn):把當前線程構(gòu)建為Node節(jié)點;判斷尾結(jié)點是否為空,通過CAS的方式將當前節(jié)點放到隊列尾部;如果尾結(jié)點不為空或者前面CAS插入尾結(jié)點失敗,調(diào)用enq方法,通過自旋的方式插入尾結(jié)點。
private Node addWaiter(Node mode) { // 1. 將當前線程構(gòu)建成Node類型 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; // 2. 1當前尾節(jié)點是否為null? if (pred != null) { // 2.2 將當前節(jié)點尾插入的方式 node.prev = pred; // 2.3 CAS將節(jié)點插入同步隊列的尾部 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
下面看下enq方法的實現(xiàn):判斷尾結(jié)點是否為空,如果為空,則通過CAS的方式創(chuàng)建一個空的頭結(jié)點(Thread為空),并將尾結(jié)點也指向頭結(jié)點;如果尾結(jié)點不為空或者上面CAS創(chuàng)建頭結(jié)點失敗,將當前隊列的前驅(qū)指針指向原來的尾結(jié)點,通過CAS的方式將當前節(jié)點放到隊列尾部,將原來尾結(jié)點的后繼指針指向當前節(jié)點;如果前面都失敗了,進行下一次循環(huán)。當前線程構(gòu)造的node節(jié)點通過addWaiter方法執(zhí)行入隊之后,其waitStatus為0,頭結(jié)點的waitStatus也是0,此時是下面這種結(jié)構(gòu)
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize //隊列為空需要初始化,創(chuàng)建空的頭節(jié)點 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; //set尾部節(jié)點 if (compareAndSetTail(t, node)) {//當前節(jié)點置為尾部 t.next = node; //前驅(qū)節(jié)點的next指針指向當前節(jié)點 return t; } } } }
通過addWaiter 方法把線程添加到鏈表后, 會接著把 Node 作為參數(shù)傳遞給acquireQueued 方法,去競爭鎖
- 1. 獲取當前節(jié)點的 prev 節(jié)點
- 2. 如果 prev 節(jié)點為 head 節(jié)點,那么它就有資格去爭搶鎖,調(diào)用 tryAcquire 搶占鎖
- 3. 搶占鎖成功以后,把獲得鎖的節(jié)點設置為 head ,并且移除原來的初始head節(jié)點,通過setHead方法和p.next=null來將新的head(獲取鎖的線程節(jié)點)與原h(huán)ead斷開連接,并將新的head的thread設為null。由此可見head節(jié)點的waitStatus都為0
- 4. 如果當前節(jié)點的前驅(qū)不是head或者當前節(jié)點是head但是獲取鎖失敗,則根據(jù)前驅(qū)節(jié)點的waitStatus(SIGNAL)決定是否需要掛起線程
/** * 已經(jīng)在隊列當中的Thread節(jié)點,準備阻塞等待獲取鎖 */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) {//死循環(huán) final Node p = node.predecessor();//找到當前結(jié)點的前驅(qū)結(jié)點 if (p == head && tryAcquire(arg)) {//如果前驅(qū)結(jié)點是頭結(jié)點,才tryAcquire,其他結(jié)點是沒有機會tryAcquire的。 setHead(node);//獲取同步狀態(tài)成功,將當前結(jié)點設置為頭結(jié)點。 p.next = null; // help GC failed = false; return interrupted; } /** * 如果前驅(qū)節(jié)點不是Head,通過shouldParkAfterFailedAcquire判斷是否應該阻塞 * 前驅(qū)節(jié)點信號量為-1,當前線程可以安全被parkAndCheckInterrupt用來阻塞線程 */ if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
下面是setHead方法的實現(xiàn)
private void setHead(Node node) { head = node; node.thread = null; node.prev = null; }
在acquireQueued(final Node node,int arg)方法中,當前線程在“死循環(huán)”中嘗試獲取同步狀態(tài),而只有前驅(qū)節(jié)點是頭節(jié)點才能夠嘗試獲取同步狀態(tài),這是為什么?原因有兩個,如下。
- 第一,頭節(jié)點是成功獲取到同步狀態(tài)的節(jié)點,而頭節(jié)點的線程釋放了同步狀態(tài)之后,將會喚醒其后繼節(jié)點,后繼節(jié)點的線程被喚醒后需要檢查自己的前驅(qū)節(jié)點是否是頭節(jié)點。
- 第二,維護同步隊列的FIFO原則。該方法中,節(jié)點自旋獲取同步狀態(tài)的行為如圖所示
如果前驅(qū)節(jié)點不是Head,通過shouldParkAfterFailedAcquire判斷是否應該阻塞:如果前驅(qū)節(jié)點waitStatus為-1(SIGNAL的狀態(tài)),當前線程可以安全的被parkAndCheckInterrupt用來阻塞線程;通過循環(huán)掃描鏈表把 CANCELLED 狀態(tài)的節(jié)點移除;如果前驅(qū)節(jié)點waitStatus不是-1,則通過CAS將前驅(qū)節(jié)點的waitStatus改為-1。
第一次循環(huán)進入shouldParkAfterFailedAcquire方法時head節(jié)點為0,會將其改為SIGNAL,此時會返回false,那么外層的方法acquireQueued方法會執(zhí)行第二次循環(huán)進入shouldParkAfterFailedAcquire方法,此時會返回true,當前線程可以被阻塞,則調(diào)用parkAndCheckInterrupt()方法阻塞當前線程,其底層調(diào)用的是UnSafe類里面的park方法。
shouldParkAfterFailedAcquire方法會將前驅(qū)節(jié)點的waitStatus改為SIGNAL,因為只有前驅(qū)節(jié)點的狀態(tài)是SIGNAL后繼節(jié)點才可以被阻塞,次數(shù)除了tail節(jié)點的狀態(tài)是0,其他的都是-1。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * 若前驅(qū)結(jié)點的狀態(tài)是SIGNAL,意味著當前結(jié)點可以被安全地park */ return true; if (ws > 0) { /* * 前驅(qū)節(jié)點狀態(tài)如果被取消狀態(tài),將被移除出隊列 */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * 當前驅(qū)節(jié)點waitStatus為 0 or PROPAGATE狀態(tài)時 * 將其設置為SIGNAL狀態(tài),然后當前結(jié)點才可以可以被安全地park */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
通過分析acquireQueued方法可以得出結(jié)論:頭結(jié)點是獲取同步狀態(tài)成功的節(jié)點,頭結(jié)點的所有有效后繼節(jié)點線程都會被阻塞,釋放鎖后需要挨個喚醒頭結(jié)點的后續(xù)線程節(jié)點。
獨占式同步狀態(tài)獲取流程,也就是acquire(int arg)方法調(diào)用流程,如圖所示。
release方法(獨占釋放)源碼分析
當前線程獲取同步狀態(tài)并執(zhí)行了相應邏輯之后,就需要釋放同步狀態(tài),使得后續(xù)節(jié)點能夠繼續(xù)獲取同步狀態(tài)。通過調(diào)用同步器的release(int arg)方法可以釋放同步狀態(tài),該方法在tryRelease方法釋放了同步狀態(tài)之后,會喚醒其后繼節(jié)點(進而使后繼節(jié)點重新嘗試獲取同步狀態(tài))。
public final boolean release(int arg) { if (tryRelease(arg)) {//釋放一次鎖 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h);//喚醒后繼結(jié)點 return true; } return false; }
該方法執(zhí)行時,會喚醒頭節(jié)點的后繼節(jié)點線程,unparkSuccessor(Node node)方法底層使用UnSafe的unpark方法來喚醒處于等待狀態(tài)的線程。
private void unparkSuccessor(Node node) { //獲取wait狀態(tài) int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0);// 將等待狀態(tài)waitStatus設置為初始值0 /** * 若后繼結(jié)點為空,或狀態(tài)為CANCEL(已失效),則從后尾部往前遍歷找到最前的一個處于正常阻塞狀態(tài)的結(jié)點 * 進行喚醒 */ 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);//喚醒線程 }
上面方法中判斷node的后繼節(jié)點是空或者waitStatus是撤銷狀態(tài),會從tail往前遍歷找到一個離node節(jié)點最近的節(jié)點,這是為什么呢? 原因在于上面acquire時調(diào)用的enq入隊方法:先compareAndSetTail(t, node)設置尾結(jié)點,然后t.next=node將前驅(qū)節(jié)點的next指針指向當前節(jié)點,如果t.next=node還沒有執(zhí)行的話,鏈表還沒有建立完整,從前向后遍歷時會出現(xiàn)遍歷到t時找不到t的后繼節(jié)點,從后往前遍歷則不會出現(xiàn)這種情況。
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize //隊列為空需要初始化,創(chuàng)建空的頭節(jié)點 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; //set尾部節(jié)點 if (compareAndSetTail(t, node)) {//當前節(jié)點置為尾部 t.next = node; //前驅(qū)節(jié)點的next指針指向當前節(jié)點 return t; } } } }
分析了獨占式同步狀態(tài)獲取和釋放過程后,適當做個總結(jié):在獲取同步狀態(tài)時,同步器維護一個同步隊列,獲取狀態(tài)失敗的線程都會被加入到隊列中(都是頭結(jié)點的后繼節(jié)點)并在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅(qū)節(jié)點為頭節(jié)點且成功獲取了同步狀態(tài)。在釋放同步狀態(tài)時,同步器調(diào)用tryRelease(int arg)方法釋放同步狀態(tài),然后喚醒頭節(jié)點的后繼節(jié)點。
3、同步隊列——共享模式源碼分析
同步隊列共享模式與獨占模式異同點:
- 共享模式跟獨占模式的相同點:獲取同步狀態(tài)失敗的線程會被包裝成node節(jié)點添加到隊列尾部
- 共享模式跟獨占模式的不同點:獨占模式中同步狀態(tài)是獨占的,只有一個線程可以獲取到同步資源,因此釋放同步資源時會喚醒head節(jié)點后面的一個節(jié)點;而共享模式因為多個線程可以共享同步資源,所以喚醒線程時會喚醒head節(jié)點后面的所有有效節(jié)點。
acquireShared方法(共享獲取)源碼分析
acquireShared方法會先調(diào)用tryAcquireShared獲取同步狀態(tài),如果返回值小于0表示獲取失敗,需要進行排隊;如果獲取成功,則可以向下執(zhí)行。
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0)//返回值小于0,獲取同步狀態(tài)失敗,排隊去;獲取同步狀態(tài)成功,直接返回去干自己的事兒。 doAcquireShared(arg); }
獲取失敗時會調(diào)用doAcquireShared方法進行排隊:
-
先通過addWaiter方法入隊,上面已經(jīng)介紹了,這里就不再重復分析了,唯一不同的就是添加的是一個共享模式的node
-
判斷當前節(jié)點的前驅(qū)節(jié)點是否是head,如果是的話再次調(diào)用tryAcquireShared獲取同步資源,如果獲取成功,則將當前node設置為head并且喚醒等待的線程節(jié)點
-
如果當前節(jié)點的前驅(qū)節(jié)點不是head或者是head但是獲取同步資源失敗,則跟上面的共享模式一樣調(diào)用shouldParkAfterFailedAcquire方法將node的前驅(qū)節(jié)點設置為SIGNAL(-1)狀態(tài),然后阻塞當前線程。
private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED);//入隊 boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor();//前驅(qū)節(jié)點 if (p == head) { int r = tryAcquireShared(arg); //非公平鎖實現(xiàn),再嘗試獲取鎖 //state==0時tryAcquireShared會返回>=0(CountDownLatch中返回的是1)。 // state為0說明共享次數(shù)已經(jīng)到了,可以獲取鎖了 if (r >= 0) {//r>0表示state==0,前繼節(jié)點已經(jīng)釋放鎖,鎖的狀態(tài)為可被獲取 //這一步設置node為head節(jié)點設置node.waitStatus->Node.PROPAGATE,然后喚醒node.thread setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } //前繼節(jié)點非head節(jié)點,將前繼節(jié)點狀態(tài)設置為SIGNAL,通過park掛起node節(jié)點的線程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
下面看下setHeadAndPropagate方法:
-
調(diào)用setHead方法將當前節(jié)點設置head
-
判斷如果需要執(zhí)行喚醒,通過上面的分析這里會調(diào)用doReleaseShared執(zhí)行喚醒
/** * 把node節(jié)點設置成head節(jié)點,且Node.waitStatus->Node.PROPAGATE */ private void setHeadAndPropagate(Node node, int propagate) { Node h = head; //h用來保存舊的head節(jié)點 setHead(node);//head引用指向node節(jié)點 /* 這里意思有兩種情況是需要執(zhí)行喚醒操作 * 1.propagate > 0 表示調(diào)用方指明了后繼節(jié)點需要被喚醒 * 2.頭節(jié)點后面的節(jié)點需要被喚醒(waitStatus<0),不論是老的頭結(jié)點還是新的頭結(jié)點 */ if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared())//node是最后一個節(jié)點或者 node的后繼節(jié)點是共享節(jié)點 /* 如果head節(jié)點狀態(tài)為SIGNAL,喚醒head節(jié)點線程,重置head.waitStatus->0 * head節(jié)點狀態(tài)為0(第一次添加時是0),設置head.waitStatus->Node.PROPAGATE表示狀態(tài)需要向后繼節(jié)點傳播 */ doReleaseShared(); } }
看下doReleaseShared方法的實現(xiàn):
判斷head!=null && head!=tail,然后判斷head的waitStatus如果是SIGNAL則會使用CAS的方式將其改為0,這里沒有直接改成PROPAGATE,是因為unparkSuccessor(h)中,如果ws < 0會設置為0,所以ws先設置為0,再設置為PROPAGATE,這里需要控制并發(fā),因為入口有setHeadAndPropagate跟release兩個,避免兩次unpark。head狀態(tài)為SIGNAL,且成功設置為0之后喚醒head.next節(jié)點線程,此時head、head.next的線程都喚醒了,head.next會去競爭鎖,成功后head會指向獲取鎖的節(jié)點,也就是head發(fā)生了變化。head發(fā)生變化后h==head會不成立了,此時會重新循環(huán),繼續(xù)喚醒重新獲取的新head的下一個節(jié)點。
如果本身頭節(jié)點的waitStatus是0,將其設置為PROPAGATE狀態(tài)。意味著需要將狀態(tài)向后一個節(jié)點傳播。
最后判斷如果h==head,即已經(jīng)沒有要喚醒的節(jié)點了,跳出循環(huán)向下執(zhí)行。如果h!=head,則說明head指針發(fā)生了變化,head已經(jīng)指向了新喚醒的線程node,繼續(xù)執(zhí)行下次循環(huán),獲取新的head,喚醒head的后續(xù)節(jié)點。
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) {//head是SIGNAL狀態(tài) /* head狀態(tài)是SIGNAL,重置head節(jié)點waitStatus為0,這里不直接設為Node.PROPAGATE, * 是因為unparkSuccessor(h)中,如果ws < 0會設置為0,所以ws先設置為0,再設置為PROPAGATE * 這里需要控制并發(fā),因為入口有setHeadAndPropagate跟release兩個,避免兩次unpark */ if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; //設置失敗,重新循環(huán) /* head狀態(tài)為SIGNAL,且成功設置為0之后,喚醒head.next節(jié)點線程 * 此時head、head.next的線程都喚醒了,head.next會去競爭鎖,成功后head會指向獲取鎖的節(jié)點, * 也就是head發(fā)生了變化。看最底下一行代碼可知,head發(fā)生變化后會重新循環(huán),繼續(xù)喚醒head的下一個節(jié)點 */ unparkSuccessor(h); /* * 如果本身頭節(jié)點的waitStatus是出于重置狀態(tài)(waitStatus==0)的,將其設置為“傳播”狀態(tài)。 * 意味著需要將狀態(tài)向后一個節(jié)點傳播 */ } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) //如果head變了,重新循環(huán) break; } }
releaseShared方法(共享釋放)源碼分析
調(diào)用tryReleaseShared方法釋放資源成功時會調(diào)用doReleaseShared方法執(zhí)行喚醒邏輯,上面已經(jīng)分析過了。
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
到此這篇關于Java并發(fā)編程之JUC并發(fā)核心AQS同步隊列原理剖析的文章就介紹到這了,更多相關JUC并發(fā)核心AQS同步隊列內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java Lambda List轉(zhuǎn)Map代碼實例
這篇文章主要介紹了Java Lambda List轉(zhuǎn)Map代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-03-03Spring Boot實現(xiàn)圖片上傳/加水印一把梭操作實例代碼
這篇文章主要給大家介紹了關于Spring Boot實現(xiàn)圖片上傳/加水印一把梭操作的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2018-11-11在SpringBoot項目中使用JetCache緩存的詳細教程
Spring Boot是一個非常流行的Java開發(fā)框架,JetCache是一個基于注解的高性能緩存框架,本文將介紹如何在Spring Boot項目中使用JetCache緩存,并提供一個詳細案例來說明如何配置和使用JetCache,需要的朋友可以參考下2024-06-06Java操作數(shù)據(jù)庫(行級鎖,for update)
這篇文章主要介紹了Java操作數(shù)據(jù)庫(行級鎖,for update),文章圍繞Java操作數(shù)據(jù)庫的相關資料展開詳細內(nèi)容,需要的小伙伴可以參考一下,希望對你有所幫助2021-12-12tomcat目錄結(jié)構(gòu)簡介_動力節(jié)點Java學院整理
這篇文章主要介紹了tomcat目錄結(jié)構(gòu)簡介_動力節(jié)點Java學院整理的相關資料,需要的朋友可以參考下2017-07-07springboot 如何解決static調(diào)用service為null
這篇文章主要介紹了springboot 如何解決static調(diào)用service為null的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06