閉鎖和柵欄的區分以及適用場景

怕翻船的忒修斯發表於2020-06-16

開篇

相信小夥伴對這個兩個詞或多或少都有些瞭解,他們是在併發程式設計中常用的執行緒通訊工具。兩者十分相似,但是又有不同,導致很多小夥伴也包括我在內產生了很多困惑:他們兩個究竟有什麼區別,以及適用於什麼場景呢?
下面聽我緩緩道來,不想看例子或者過程的小夥伴可以拉到最下面看總結呦

閉鎖

閉鎖(CountDownLatch)坊間俗稱計數器,官方(谷歌機翻,哈哈)解釋:

/**
 * A synchronization aid that allows one or more threads to wait until
 * a set of operations being performed in other threads completes.
 */

允許一個或多個執行緒等待,直到在其他執行緒中執行的一組操作完成的同步輔助程式。

大概意思就是說,可以有一個或者多個執行緒,等待其他執行緒都完成某個操作後,再繼續執行。
什麼意思呢?舉個例子吧:
生活中應該經常遇見一種情況,坐公交車是,尤其是始發站,司機師傅往往為了一次拉更多的乘客,會等到車上乘客的數量到達一定程度以後才會發車。測試程式碼如下:

public static void main(String[] args) {
        List<Passenger> list = new ArrayList<>();
        Passenger p1 = new Passenger("看會書");
        Passenger p2 = new Passenger("看會手機");
        Passenger p3 = new Passenger("看會風景");
        Passenger p4 = new Passenger("看會售票員");
        list.add(p1);
        list.add(p2);
        list.add(p3);
        list.add(p4);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 200, 1000, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3), new ThreadFactory() {
            private ThreadGroup group = (null == System.getSecurityManager() ? Thread.currentThread().getThreadGroup() : System.getSecurityManager().getThreadGroup());
            private AtomicInteger num = new AtomicInteger();
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(group, r,"zoo" + num.getAndIncrement(),0);
                thread.setDaemon(false);
                return thread;
            }
        }, new ThreadPoolExecutor.CallerRunsPolicy());
        //設定閉鎖釋放閾值
        CountDownLatch countDownLatch = new CountDownLatch(list.size());
        log.error("司機師傅人夠一車再發車,等會人吧...");
        for (Passenger p : list) {
            executor.execute(()->gotoZOO(p,countDownLatch));
        }
        try {
            countDownLatch.await();
            log.error("人夠了,起飛!");
            executor.shutdown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    private static void  gotoZOO(Passenger p,CountDownLatch countDownLatch){
        log.error("{}的乘客上車啦",p.getDoWhat());
        try {
            countDownLatch.countDown();
            log.error("{}",p.doWhatOnBus());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class Passenger{
        private String doWhat;

        public Passenger(String doWhat) {
            this.doWhat = doWhat;
        }

        public String getDoWhat() {
            return doWhat;
        }

        public String doWhatOnBus() {
            return "車上好無聊啊,"+doWhat+"吧!";
        }
    }

執行結果

23:46:34.698 [main] ERROR com.test - 司機師傅人夠一車再發車,等會人吧...
23:46:34.757 [zoo1] ERROR com.test - 看會手機的乘客上車啦
23:46:34.758 [zoo3] ERROR com.test - 看會售票員的乘客上車啦
23:46:34.757 [zoo0] ERROR com.test - 看會書的乘客上車啦
23:46:34.759 [zoo1] ERROR com.test - 車上好無聊啊,看會手機吧!
23:46:34.759 [zoo3] ERROR com.test - 車上好無聊啊,看會售票員吧!
23:46:34.757 [zoo2] ERROR com.test - 看會風景的乘客上車啦
23:46:34.759 [zoo0] ERROR com.test - 車上好無聊啊,看會書吧!
23:46:34.759 [zoo2] ERROR com.test - 車上好無聊啊,看會風景吧!
23:46:34.759 [main] ERROR com.test - 人夠了,起飛!

司機師傅(主執行緒)要等上了4個乘客以後才發車(等待4個子執行緒完成完成某件事以後呼叫countDown方法),而乘客上車(呼叫countDown)以後該做自己的事還做自己的事情,不會因為上了車就傻呆呆的什麼都不幹了(不會因為呼叫了countDown而阻塞自身)。等司機師傅看人夠了(到達設定閾值),就發車了。

閉鎖總結:
主執行緒呼叫await後會阻塞等待其他子執行緒呼叫countDown方法將設定閾值減至0,然後在繼續執行。
而子執行緒不會因為呼叫了countDown方法而阻塞

柵欄

柵欄(CyclicBarrier)官方解釋:

/**
 * A synchronization aid that allows a set of threads to all wait for
 * each other to reach a common barrier point.  CyclicBarriers are
 * useful in programs involving a fixed sized party of threads that
 * must occasionally wait for each other. The barrier is called
 * <em>cyclic</em> because it can be re-used after the waiting threads
 * are released.
 */
同步幫助,允許一組執行緒互相等待,以達到共同的障礙點。 CyclicBarriers在涉及固定大小的執行緒方的程式中很有用,這些執行緒有時必須互相等待。該屏障稱為<em> cyclic </ em>,因為它可以在釋放等待執行緒後重新使用。

從類註釋上我們可以大致瞭解到,他是運用在一組,也即是多個執行緒中的,當所有執行緒到達某個狀態前一直阻塞,直到所有執行緒都達到後再繼續執行。而且是可以重複使用的。

上面的描述還是太晦澀了,還是舉個例子:
我們小時候學校都組織過春遊,規定好地點,等人到齊了就一起進去玩。寫了個簡單的例子,看這種場景柵欄是怎麼工作的

public static void main(String[] args) {
        List<Boy> list = new ArrayList<>();
        Boy boy1 = new Boy("看老虎");
        Boy boy2 = new Boy("看猩猩");
        Boy boy3 = new Boy("看獅子");
        Boy boy4 = new Boy("看售票員");
        list.add(boy1);
        list.add(boy2);
        list.add(boy3);
        list.add(boy4);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 200, 1000, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3), new ThreadFactory() {
            private ThreadGroup group = (null == System.getSecurityManager() ? Thread.currentThread().getThreadGroup() : System.getSecurityManager().getThreadGroup());
            private AtomicInteger num = new AtomicInteger();
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(group, r,"zoo" + num.getAndIncrement(),0);
                thread.setDaemon(false);
                return thread;
            }
        }, new ThreadPoolExecutor.CallerRunsPolicy());
        //初始化柵欄,設定障礙點閾值
        CyclicBarrier cyclicBarrier = new CyclicBarrier(list.size());
        for (Boy boy : list) {
            executor.execute(()->gotoZOO(boy,cyclicBarrier));
        }
    }

    private static void  gotoZOO(Boy boy,CyclicBarrier cyclicBarrier){
        log.error("人還沒到齊呢,等一下吧,{}的小男孩開始等待",boy.getWhere());
        try {
            cyclicBarrier.await();
            log.error("{}",boy.goWhere());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class Boy{
        private String where;
        public Boy(String where) {
            this.where = where;
        }

        public String getWhere() {
            return where;
        }

        public String goWhere() {
            return "人到齊了,我要去"+where+"啦!";
        }
    }

執行結果:

22:05:59.476 [zoo2] ERROR com.test - 人還沒到齊呢,等一下吧,看獅子的小男孩開始等待
22:05:59.477 [zoo1] ERROR com.test - 人還沒到齊呢,等一下吧,看猩猩的小男孩開始等待
22:05:59.477 [zoo0] ERROR com.test - 人還沒到齊呢,等一下吧,看老虎的小男孩開始等待
22:05:59.476 [zoo3] ERROR com.test - 人還沒到齊呢,等一下吧,看售票員的小男孩開始等待
22:05:59.484 [zoo0] ERROR com.test - 人到齊了,我要去看老虎啦!
22:05:59.484 [zoo2] ERROR com.test - 人到齊了,我要去看獅子啦!
22:05:59.484 [zoo3] ERROR com.test - 人到齊了,我要去看售票員啦!
22:05:59.484 [zoo1] ERROR com.test - 人到齊了,我要去看猩猩啦!

我們可以發現前三個小男孩在到達以後都沒有進到動物園裡,而是直到第四個小男孩來到以後,四個小男孩才進入動物園,在此之前每來一個小朋友就多一個小朋友等待(每個執行緒呼叫await方法),直到等待所有人到齊(執行緒阻塞等待達到柵欄障礙點4),各個小男孩再去繼續進入動物園看動物(各執行緒繼續執行自己的任務)。就像是動物園大門的柵欄,買的是團體票,每次必須人到齊才放開讓小朋友進去一樣。

柵欄總結
各子執行緒相互等待,直到達到柵欄初始化時的閾值,則繼續執行

區分以及個人理解

閉鎖:有點類似於一個統計功能(可能這也是為什麼他俗稱計數器),主執行緒呼叫await方法阻塞等待統計結果,而子執行緒只負責在達到統計要求時呼叫countDown方法告訴主執行緒我好了,而不會阻塞本身;有一個負責接收結果(主執行緒)和一個或多個傳送數量的(子執行緒);
柵欄:首先線上程呼叫await方法時會阻塞當前執行緒,其次個人理解他沒有類似像閉鎖那樣的主子的關係,他是各個執行緒相互等待,都到達某個點的時候,則繼續執行。

適用場景

其實從上面的區分就能看出一些:如果是需要將多執行緒執行完成與否的介面彙總到某一個執行緒中,然後再繼續執行的情況,比如每條執行緒計算一個指標,都計算完成以後再計算所有指標的總和或者其他的,就可以使用閉鎖;
而如果只是各個執行緒需要等各個執行緒都完成了,再繼續自己的事,可以使用柵欄,比如ABC三個執行緒分別去獲取123三個指標,然後再A要取這三個數的平均數,B要取總和,C要取方差,那就需要等ABC都先取完了123這三個指標,才能計算,這時候就可以用到柵欄了。

總結

這兩種都是非常好的執行緒通訊工具,不過細節還是有所差異。
總得來說就是:

閉鎖是為了在某一條執行緒等待獲取到其他執行緒的執行結果;
而柵欄則是執行緒間的相互等待,然後再同時開始做各自的事情

最後

文中的程式碼只是為了比較好的說明兩種工具的差異,寫的不好還請小夥伴們多多包涵,如果發現有哪點寫的不對的也歡迎大傢伙們留言,我們共同進步!最後如果小夥伴覺得文章不錯,不妨動動小手點個贊再走,不要下次一定呦~


最後的最後

給走過路過的小夥伴們推薦一個公眾號,”菜鳥封神記“,裡面幹活滿滿,最新在更新spring原始碼相關的文章,感興趣的小夥伴不妨關注一下,微信搜尋公眾號:”菜鳥封神記“,或者掃描下面公眾號關注即可。2223.png

相關文章