在上一篇CountDownLatch解析中,我們了解了CountDownLatch的簡介、CountDownLatch實用場景、CountDownLatch實現原理中的await()方法,
接下來我們接著來了解它的countDown()方法以及它的示例和優缺點。
二、CountDownLatch.countDown()
關於同步隊列那點事
當部分線程調用await()方法後,它們在同步隊列中被掛起,然後自省的檢查自己能否滿足醒來的條件(還記得那個條件嗎?1、state為0,2、該節點為頭節點),
如果滿足它將被移除頭節點,並將下一個節點設置為頭節點。具體如下圖:
這個Node是AbstractQueuedSynchronizer(下文簡稱AQS)中的靜態內部類,其中它又有兩個屬性:
volatile Node prev; volatile Node next;
volatile的prev指向上一個node節點,volatile的next指向下一個node節點。當然如果是頭節點,那麼它的prev為null,同理尾節點的next為null。
private transient volatile Node head; private transient volatile Node tail;
head和tail是AQS中的屬性,它們用來表示同步隊列的頭節點和尾節點。(有興趣的同學可以引申看一下 transident 這個關於序列化關鍵字,這裡不展開啦=_=)
了解了同步隊列後,接著我們開始線程調用countDown()方法後,到底發生了什麼事?這兒咱們可以想象一下,可能計數值會減去1,而且還會判斷state是不是等於0,
如果不等於0,這個線程就繼續往下走了;如果等於0,那麼它可能還需要去叫醒這群掛起的線程。
到底是不是這樣呢,別方,跟著筆者一起繼續來扒源碼。
public void countDown() { sync.releaseShared(1); }
當調用CountDownLatch.countDown()後,它轉而調用了Sync這個內部類實例的releaseShared()方法。
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; //退出該方法 } return false; //退出該方法 }
在Sync類中並沒有releaseShared()方法,所以應該是繼承與AQS,咱們看到AQS這個方法中,退出該方法的只有兩條路。tryReleaseShared(arg)條件為真執行一個doReleaseShared()退出;條件為假直接退出。
類比我們的猜測,這個條件很有可能就是state是否為0的判斷。。。接著來看。
protected boolean tryReleaseShared(int releases) { for (;;) {//死循環 int c = getState();// 獲取主存中的state值 if (c == 0) //state已經為0 直接退出 return false; int nextc = c-1; // 減一 准備cas更新該值 if (compareAndSetState(c, nextc)) //cas更新 return nextc == 0; //更新成功 判斷是否為0 退出;更新失敗則繼續for循環,直到線程並發更新成功 } }
看到這兒四不四靈光從腦子噴湧而出啦。我們的猜測是正確的!
private void doReleaseShared() { for (;;) {//又是一個死循環 Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) {//如果當前節點是SIGNAL意味著,它正在等待一個信號,或者說它在等待被喚醒,因此做兩件事,1是重置waitStatus標志位,2是重置成功後,喚醒下一個節點。 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; unparkSuccessor(h); }else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))//如果本身頭節點的waitStatus是出於重置狀態(waitStatus==0)的,將其設置為“傳播”狀態。意味著需要將狀態向後一個節點傳播。 continue; } if (h == head) break; } }
同學們(敲黑板...),我們為啥要執行這個方法呀,因為state已經為0啦,我們該將同步隊列中的線程狀態設置為共享狀態(Node.PROPAGATE,默認狀態ws == 0),並向後傳播,實現狀態共享。
這就是為啥方法名有個shared,因為這個共享狀態需要傳播下去,而不是一個節點(線程)獨占。看看這個死循環,退出的路只有一條,那就是h==head,即該線程是頭節點,且狀態為共享狀態。
這裡再啰嗦一句,可能讀者會問,state已經等於0了,我們也通過循環的方式把頭節點的狀態設置為共享狀態,但是它怎麼醒過來的呢?既然這樣,那我們再看一次上一篇中的代碼
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;// 計數值為0 並且為頭節點 跳出循環 } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException();// 響應打斷 跳出循環 } } finally { if (failed) cancelAcquire(node);// 如果是打斷退出的 則移除同步隊列節點 } }
這個就是在同步隊列中掛起的線程,它們自旋的形式查看自己是否滿足條件醒來(state==0,且為頭節點),如果成立將調用setHeadAndPropagate這個方法
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); }
}
這個方法是將當前節點的下一個節點設置為頭節點,且它也調用了doReleaseShared這個方法,我們剛才也說了,這個方法就是將頭節點設置為共享狀態的,由此,共享狀態傳播下去。
至此,CountDownLatch實現原理中的countDown()方法剖析結束。
廢話不多說,直接上代碼:
package com.test.demo; import java.util.concurrent.CountDownLatch; /** * @Title: TestCountDownLatch.java * @Describe: * @author: Mr.Yanphet * @Email: [email protected] * @date: 2016年9月18日 上午11:22:42 * @version: 1.0 */ public class TestCountDownLatch { private static final int taskNum = 5; static class MyRunnable implements Runnable { private int num; private CountDownLatch cdl; public MyRunnable(int num, CountDownLatch cdl){ this.num = num; this.cdl = cdl; } public void run() { System.out.println("第" + num + "個線程開始執行任務..."); try { Thread.sleep(5 * 1000); // 模擬任務耗時 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("第" + num + "個線程任務執行結束..."); cdl.countDown(); } } public static void main(String[] args) { CountDownLatch cdl = new CountDownLatch(taskNum); for (int i = 1; i <= taskNum; i++) { MyRunnable mr = new MyRunnable(i, cdl); Thread t = new Thread(mr); t.start(); } System.out.println("等待其他線程完成任務才繼續執行..."); try { cdl.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("其他線程完成任務,主線程開始執行..."); System.out.println("主線程任務完成,整個任務進度完成..."); } }
執行結果如下:
第4個線程開始執行任務... 第5個線程開始執行任務... 第3個線程開始執行任務... 第2個線程開始執行任務... 第1個線程開始執行任務... 等待其他線程完成任務才繼續執行... 第4個線程任務執行結束... 第2個線程任務執行結束... 第1個線程任務執行結束... 第3個線程任務執行結束... 第5個線程任務執行結束... 其他線程完成任務,主線程開始執行... 主線程任務完成,整個任務進度完成...
主線程等待5個子線程執行完任務,才繼續往下執行自己的任務。
優點:
CountDownLatch的優點毋庸置疑,對使用者而言,你只需要傳入一個int型變量控制任務數量即可,至於同步隊列的出隊入隊維護,state變量值的維護對使用者都是透明的,使用方便。
缺點:
CountDownLatch設置了state後就不能更改,也不能循環使用。
以上就是關於CountDownLatch學習的全部內容。因為筆者也是菜鳥,所以站在菜鳥的角度分析源碼,難免重復啰嗦。如有任何問題,希望大家指正。謝謝~~~