Java并發編程——CountDownLatch

一、閉鎖 CountDownLatch

一個同步工具類,允許一個或者多個線程一直等待,直到其他線程的操作都執行完成之后再繼續往下執行。

使用場景:在一些應用場合中,需要等待某個條件達到要求后才能做后面的事情;同時當線程都完成后也會觸發事件,以便進行后面的操作。 這個時候就可以使用CountDownLatch。

CountDownLatch最重要的方法是countDown()和await(),前者主要是計數減一,后者是等待計數到0,如果沒有到達0,就繼續阻塞等待。

countdownlatch

如上圖,左邊三只小熊,可以當成三個線程,每一只撞到欄桿,計數器就減1,這相當于執行了countDown方法;

右邊有兩只暴走小熊在等待計數器變為0,可以當成兩個線程,執行了await方法;

最終左邊三只暴走小熊抵達了欄桿處,計數器變為0,喚醒了右邊的暴走小熊,暴走小熊就開始動起來了。

二、執行原理

CountDownLatch是基于AQS共享模式的使用。

如下圖,我們通過給CountDownLatch構造函數傳入state的值。

countDown方法本質是釋放共享鎖,核心實現邏輯是:state>0 && state-1,如果state>0,則state減一,否則執行失敗;

await方法本質是獲取共享鎖,核心實現是:getState()==0,如果state==0,則表示獲取成功,否則線程阻塞進入等待隊列;

當state減到0的時候,會喚醒等待隊列中的所有線程,嘗試繼續獲取共享鎖,這個時候正常是所有線程都能獲取成功的。

三、CountDownLatch的用法

3.1 CountDownLatch典型用法1

某一線程在開始運行前等待n個線程執行完畢。將CountDownLatch的計數器初始化為new CountDownLatch(n),每當一個任務線程執行完畢,就將計數器減1 countdownLatch.countDown(),當計數器的值變為0時,在CountDownLatch上await()的線程就會被喚醒。一個典型應用場景就是啟動一個服務時,主線程需要等待多個組件加載完畢,之后再繼續執行。

/**
 * @Description: 工廠中,質檢,5個工人檢查,所有人都認為通過,才通過
 */
public class CountDownLatchDemo {
    
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);

        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i+1;
            executorService.submit(() -> {
                try {
                    Thread.sleep(new Random().nextInt(10000));
                    System.out.println("NO." + no + "完成了檢查");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        System.out.println("等待5個人檢查完......");

        countDownLatch.await();
        System.out.println("所有人都完成了工作,等待進入下一環節");
    }
}

3.2 CountDownLatch典型用法2

實現多個線程開始執行任務的最大并行性。注意是并行性,不是并發,強調的是多個線程在某一時刻同時開始執行。類似于賽跑,將多個線程放到起點,等待發令槍響,然后同時開跑。做法是初始化一個共享的CountDownLatch(1),將其計算器初始化為1,多個線程在開始執行任務前首先countdownlatch.await(),當主線程調用countDown()時,計數器變為0,多個線程同時被喚醒。

/**
 * @Description: 模擬100米跑步,5名選手都準備好了,只等裁判員一聲令下,所有人同時開跑
 */
public class CountDownLatchDemo2 {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);

        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i+1;
            executorService.submit(() -> {
                System.out.println("No." + no + ",準備完畢,等待發令槍");
                try {
                    countDownLatch.await();
                    System.out.println("No." + no + ",開始跑步");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        //裁判員檢查發令槍......
        Thread.sleep(5000);
        System.out.println("發令槍響,比賽開始......");
        countDownLatch.countDown();
    }
}

3.3 CountDownLatch兩種用法結合使用

/**
 * @Description: 模擬100米跑步,5名選手都準備好了,只等裁判員一聲令下,所有人同時開跑,
 * 所有人都到終點后,比賽結束
 */
public class CountDownLatchDemo3 {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch begin = new CountDownLatch(1);
        CountDownLatch end = new CountDownLatch(5);

        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i+1;
            executorService.submit(() -> {
                System.out.println("No." + no + ",準備完畢,等待發令槍");
                try {
                    begin.await();
                    System.out.println("No." + no + ",開始跑步");
                    Thread.sleep(new Random().nextInt(10000));
                    System.out.println("No." + no + ",跑到終點了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    end.countDown();
                }
            });
        }

        //裁判員檢查發令槍......
        Thread.sleep(5000);
        System.out.println("發令槍響,比賽開始......");
        begin.countDown();

        //等待5個線程都執行完畢之后
        end.await();
        System.out.println("所有人到達終點,比賽結束");
    }
}

3.4 CountDownLatch注意點

  • 擴展用法:多個線程等待多個線程完成執行后,再同時執行。
  • CountDownLatch是不能夠重用的,如果需要重新計數,可以考慮使用CyclicBarrier或者創建新的CountDownLatch實例。

四、源碼

4.1 構造方法:創建一個Sync對象,而Sync繼承AQS

public class CountDownLatch {
    
    private final Sync sync;
    
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        //設置計數器實際上是將值賦給了AQS狀態變量state
        this.sync = new Sync(count);
    }
}

4.2 Sync 是CountDownLatch的內部私有類,組合到CountDownLatch里

public class CountDownLatch {
    
    private static final class Sync extends AbstractQueuedSynchronizer {
        //設置計數器實際上是將值賦給了AQS狀態變量state
        Sync(int count) {
            setState(count);
        }

        //獲取狀態變量state的值
        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        protected boolean tryReleaseShared(int releases) {
            //循環進行CAS,直到當前線程完成CAS減去1操作
            for (;;) {
                int c = getState();
                //當前狀態值為0則直接返回
                if (c == 0)
                    return false;
                int nextc = c-1;
                //使用CAS讓計數器值減去1
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

    private final Sync sync;
}

在AQS中state是一個private volatile int類型的變量。CountDownLatch使用state來計數,CountDownLatch的getCount最終調用的是AQS的getState(),返回state進行計數。

4.3 await()方法:調用AQS的acquireSharedInterruptibly方法

public class CountDownLatch {
    
    private final Sync sync;
    
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }   
}
  • acquireSharedInterruptibly方法:獲取共享鎖
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    //獲取共享鎖
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //判斷線程是否為中斷狀態,如果是拋出interruptedException 
        if (Thread.interrupted())
            throw new InterruptedException();
        //嘗試獲取共享鎖,嘗試成功就返回,否則調用doAcquireSharedInterruptibly方法    
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
}
  • tryAcquireShared方法:嘗試獲取共享鎖
public class CountDownLatch {
    
    private final Sync sync;

    private static final class Sync extends AbstractQueuedSynchronizer {
        //嘗試獲取共享鎖,重寫AQS里面的方法
        protected int tryAcquireShared(int acquires) {
            //鎖狀態 == 0,表示所沒有被任何線程所獲取,
            //即是可獲取的狀態,否則鎖是不可獲取的狀態
            return (getState() == 0) ? 1 : -1;
        }
    }
}
  • doAcquireSharedInterruptibly方法:會使得當前線程一直等待,直到當前線程獲取到鎖(或被中斷)才返回
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //創建“當前線程”的Node節點,且node中記錄的鎖是“共享鎖”類型,并將節點添加到CLH隊列末尾。
        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) {
                        // 獲取成功,把當前節點變為新的head節點,
                        //并且檢查后續節點是否可以在共享模式下等待,
                        //并且允許繼續傳播,則調用doReleaseShared繼續喚醒下一個節點嘗試獲取鎖
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //前繼節點不是頭節點,當前線程一直等待,直到獲取到鎖
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
}
  • shouldParkAfterFailedAcquire方法:判斷當前線程獲取鎖失敗之后是否需要掛起
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
 /*說明:4.shouldParkAfterFailedAcquire 返回當前線程是否應該阻塞
    (01) 關于waitStatus請參考下表(中擴號內為waitStatus的值)

    CANCELLED[1]  -- 當前線程已被取消
    SIGNAL[-1]    -- “當前線程的后繼線程需要被unpark(喚醒)”。
                        一般發生情況是:當前線程的后繼線程處于阻塞狀態,
                        而當前線程被release或cancel掉,因此需要喚醒當前線程的后繼線程。
    CONDITION[-2] -- 當前線程(處在Condition休眠狀態)在等待Condition喚醒
    PROPAGATE[-3] -- (共享鎖)其它線程獲取到“共享鎖”
    [0]           -- 當前線程不屬于上面的任何一種狀態。
    
    (02) shouldParkAfterFailedAcquire()通過以下規則,判斷“當前線程”是否需要被阻塞。

    規則1:如果前繼節點狀態為SIGNAL,表明當前節點需要被unpark(喚醒),此時則返回true。
    規則2:如果前繼節點狀態為CANCELLED(ws>0),說明前繼節點已經被取消,則通過先前回溯找到一個有效(非CANCELLED狀態)的節點,并返回false。
    規則3:如果前繼節點狀態為非SIGNAL、非CANCELLED,則設置前繼的狀態為SIGNAL,并返回false。
    */  
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 前驅節點的狀態
        int ws = pred.waitStatus;
        // 如果前驅節點是SIGNAL狀態,則意味著當前線程需要unpark喚醒,此時返回true
        if (ws == Node.SIGNAL)
            return true;
            
        // 如果前繼節點是取消的狀態即前驅節點狀態為CANCELLED 
        if (ws > 0) {
            // 從隊尾向前尋找第一個狀態不為CANCELLED的節點
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 將前驅節點的狀態設置為SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }   
}   

4.4 countDown()源碼

public class CountDownLatch {
    
    private final Sync sync;

    public void countDown() {
        //該方法其實調用AQS中的releaseShared(1)釋放共享鎖方法。
        sync.releaseShared(1);
    }
}
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    //目的是讓當前線程釋放它所持有的共享鎖,它首先會通過tryReleaseShared()去嘗試釋放共享鎖。
    //嘗試成功,則直接返回;嘗試失敗,則通過doReleaseShared()去釋放共享鎖。
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
}
  • tryReleaseShared()在CountDownLatch.java中被重寫,釋放共享鎖,將鎖計數器-1
public class CountDownLatch {
    
    private final Sync sync;

    private static final class Sync extends AbstractQueuedSynchronizer {
    
        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                // 獲取“鎖計數器”的狀態
                int c = getState();
                if (c == 0)
                    return false;
                // “鎖計數器”-1 
                int nextc = c-1;
                // 通過CAS函數進行賦值。
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }
}

參考:
https://www.itzhai.com/articles/graphical-several-fun-concurrent-helper-classes.html

https://zhuanlan.zhihu.com/p/95835099

https://www.cnblogs.com/200911/p/6059719.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容