Java中關於線程同步,你會用到的4個類

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();
    }

運行結果:

我是賣家,我帶着裝備過來了!
我是買家,我帶着一萬遊戲幣過來了
賣家達到交易地點
買家到達交易地點
我是買家,換回了裝備
我是賣家,換回了一萬遊戲幣

往期推薦


數據庫消耗 CPU 最高的 sql 語句如何定位?

怎麼樣通過Nginx實現限流?

5個好用的開源網絡監控工具

SpringBoot實現登錄攔截的原理

keepalived實現Nginx雙機熱備

一行代碼讓你擺脫U盤完成局域網文件傳輸

Docker簡易搭建 ElasticSearch 集羣

我把跨境當副業,一個星期賺了7000塊”:想給有夢想的人提個醒!!!

這個基於jedis的Redis工具類可以收藏一下

ZooKeeper實現分佈式隊列、分佈式鎖和選舉,很詳細

Spring SchedulingConfigurer 實現動態定時任務

從原則、方案、策略及難點闡述分庫分表

(建議收藏)服務器宕機了不要慌,這樣排查效率高


本文分享自微信公衆號 - 俠夢的開發筆記(xmdevnote)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章