一文搞懂四種同步工具類

_ 作者:_CoderV 的進階筆記

https://juejin.cn/post/6844903958360621064

CountDownLatch

解釋:

CountDownLatch 相當於一個門閂,門閂上掛了 N 把鎖。只有 N 把鎖都解開的話,門纔會打開。怎麼理解呢?我舉一個賽跑比賽的例子,賽跑比賽中必須等待所有選手都準備好了,裁判才能開發令槍。選手纔可以開始跑。

CountDownLatch 當中主要有兩個方法,一個是 await() 會掛上鎖阻塞當前線程,相當於裁判站在起始點等待,等待各位選手準備就緒,一個是 countDown 方法用於解鎖,相當於選手準備好了之後調用 countDown 方法告訴裁判自己準備就緒,當所有人都準備好了之後裁判開發令槍。

代碼:

public class TestCountDownLatch {
    public static void main(String[] args) {
        // 需要等待兩個線程,所以傳入參數爲2
        CountDownLatch latch = new CountDownLatch(2);
        // 該線程運行1秒
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("1號選手準備就緒!用時1秒!");
                latch.countDown();
            }
        }).start();
        
        // 該線程運行3秒
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("2號選手準備就緒!用時3秒!");
                latch.countDown();
            }
        }).start();
        
        try {
            System.out.println("請1號選手和2號選手各就各位!");
            // 主線程在此等待兩個線程執行完畢之後繼續執行
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 兩個線程執行完畢後,主線程恢復運行
        System.out.println("裁判發槍,1號選手和2號選手開跑!");
    }
}

運行結果:

請1號選手和2號選手各就各位!
1號選手準備就緒!用時1秒!
2號選手準備就緒!用時3秒!
裁判發槍,1號選手和2號選手開跑!

如果去掉 CountDownLatch 的效果呢?運行結果就會變成如下:

請1號選手和2號選手各就各位!
裁判發槍,1號選手和2號選手開跑!
1號選手準備就緒!用時1秒!
2號選手準備就緒!用時3秒!

裁判就會在選手還未準備就緒的時候開發令槍,這就亂套了。
其實 CountDownLatch 一個最簡單的用處就是計算多線程執行完畢時的時間。像剛纔的例子當中兩個線程並行執行了共花費了 3 秒鐘。

CyclicBarrier

解釋:

CyclicBarrier 就像一個柵欄,將各個線程攔住。Cyclic 是循環的英文,表明該工具可以進行循環使用。CyclicBarrier(N) 的構造參數表明該一共有幾個線程需要互相等待。它相當於 N 個選手約定進行多次比賽,每次比賽完都要在起跑點互相等待。

讀者可能會馬上疑惑這不是和 CountDownLatch 一樣嗎?不一樣。因爲 CountDownLatch 是裁判等待選手,是調用 await() 方法的線程,等待調用 countDown() 方法的各個線程。而 CyclicBarrier 是選手等待選手,是調用 await() 方法的線程互相等待,等待其他線程都運行好之後,再開始下一輪運行。

我們舉一個例子,兩個選手進行比賽,一共進行三輪比賽。

代碼:

public class TestCyclicBarrier {
    // 1號選手跑的輪數
    public static int countA = 1;
    // 2號選手跑的輪數
    public static int countB = 1;
    public static void main(String[] args) {
        // 填入2,代表2個線程互相等待
        CyclicBarrier barrier = new CyclicBarrier(2);

        new Thread(new Runnable() {
            @Override
            public void run() {
                // 一共跑三輪
                for (int i = 0; i < 3; i++) {
                    System.out.println("1號選手開始跑!當前第" + countA++ + "輪比賽!");
                    // 1號選手跑得慢,每次跑三秒
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        System.out.println("1號選手抵達終點!");
                        // 調用等待方法,在此等待其他選手
                        barrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                // 一共等待三輪
                for (int i = 0; i < 3; i++) {
                    System.out.println("2號選手開始跑!當前第" + countB++ + "輪比賽!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        System.out.println("2號選手抵達終點!");
                        // 調用等待方法,在此等待其他選手
                        barrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

}

運行結果:

1號選手開始跑!當前第1輪比賽!
2號選手開始跑!當前第1輪比賽!
2號選手抵達終點!
1號選手抵達終點!
1號選手開始跑!當前第2輪比賽!
2號選手開始跑!當前第2輪比賽!
2號選手抵達終點!
1號選手抵達終點!
1號選手開始跑!當前第3輪比賽!
2號選手開始跑!當前第3輪比賽!
2號選手抵達終點!
1號選手抵達終點!

每輪比賽 1 號選手和 2 號選手都會回到起跑線互相等待,再開啓下一輪比賽。

如果不加 CyclicBarrier 呢?

1號選手開始跑!當前第1輪比賽!
2號選手開始跑!當前第1輪比賽!
2號選手抵達終點!
2號選手開始跑!當前第2輪比賽!
2號選手抵達終點!
2號選手開始跑!當前第3輪比賽!
1號選手抵達終點!
1號選手開始跑!當前第2輪比賽!
2號選手抵達終點!
1號選手抵達終點!
1號選手開始跑!當前第3輪比賽!
1號選手抵達終點!

此時 2 號選手就直接跑完三輪比賽,不等 1 號選手了。

Semaphore

Semaphore 英文的字面意思是信號量。它的工作機制是每個線程想要獲取運行的機會的話,都必須獲取到信號量。acquire() 方法阻塞的獲取信號量,release() 釋放信號量。

舉個例子,假設我們去迪士尼遊玩,但是迪士尼擔心遊客很多的話,影響大家的遊玩體驗,於是規定每個小時只能賣出兩張門票。這樣就可以控制在遊樂園當中的遊客數量了。

代碼:

public class TestSemaphore {

    public static void main(String[] args) {

        Semaphore semaphore = new Semaphore(0);
        System.out.println("顧客在售票處等候中");
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (; ; ) {
                    try {
                        Thread.sleep(500);
                        // 等待出票
                        semaphore.acquire();
                        System.out.println("顧客拿到門票入場!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {

                for (int i = 0; i < 3; i++) {
                    try {
                        // 等待一小時再發門票
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 一次性發出兩張門票
                    System.out.println("售票處第" + (i + 1) + "小時售出兩張票!");
                    semaphore.release();
                    semaphore.release();
                }
            }
        }).start();

        System.out.println("售票處開始售票!");
    }
}

運行結果:

顧客在售票處等候中...
售票處開始售票!
售票處第1小時售出兩張票!
顧客拿到門票入場!
顧客拿到門票入場!
售票處第2小時售出兩張票!
顧客拿到門票入場!
顧客拿到門票入場!
售票處第3小時售出兩張票!
顧客拿到門票入場!
顧客拿到門票入場!

Exchanger

解釋:

Exchanger 提供了讓兩個線程互相交換數據的同步點。Exchanger 有點像 2 個線程的 CyclicBarrier,線程之間都是互相等待,區別在於 Exchanger 多了交換的操作。

舉個例子好比以前玩網遊的時候,買家和賣家必須走到地圖上同一地點面對面進行交易一樣,一手交錢一手交裝備。

代碼:

public class TestExchanger {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                String weapon = "裝備";
                System.out.println("我是賣家,我帶着" + weapon + "過來了!");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("賣家到達地圖上交易地點");
                try {
                    System.out.println("我是賣家,換回了" + exchanger.exchange(weapon));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                String money = "一萬遊戲幣";
                System.out.println("我是買家,我帶着" + money + "過來了");
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("買家到達地圖上交易地點");
                try {
                    System.out.println("我是買家,換回了" + exchanger.exchange(money));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }

運行結果:

我是賣家,我帶着裝備過來了!
我是買家,我帶着一萬遊戲幣過來了
賣家達到交易地點
買家到達交易地點
我是買家,換回了裝備
我是賣家,換回了一萬遊戲幣
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/j2EtFf_hOJm9L_y2jje7uQ