java併發三劍客之CyclicBarrier、CountDownLatch、Semaphore

看了大佬的專欄,https://blog.csdn.net/heihaozi/category_10085170.html。感嘆,寫的真好,清楚明晰。決定用自己的邏輯總結記錄下。

CyclicBarrier

CyclicBarrier: 循環柵欄,通過它可以實現讓一組線程等待至某個狀態之後再全部同時執行。這個狀態可以說是barrier,當調用await之後,線程就處於barrier狀態了。
怎麼理解循環?
循環是因爲當所有等待線程都被釋放以後,CyclicBarrier可以被重用。

舉個例子:
假設小明、小紅、小亮兄妹三個要喫早喫飯,媽媽說先洗手,洗完手之後大家一起喫,等三個人喫完飯,再一起去玩。在這個例子中第一個barrier狀態是大家都洗好手,第二個barrier狀態是大家都喫完飯。第二個barrier在第一個barrier釋放後可以重用。

CyclicBarrierTest.java

package com.example.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier barrier  = new CyclicBarrier(3);
        List<Thread> threads = new ArrayList<>(3);
        threads.add(new Thread(new Child(barrier, "小明")));
        threads.add(new Thread(new Child(barrier, "小紅")));
        threads.add(new Thread(new Child(barrier, "小亮")));

        for (Thread thread : threads) {
            thread.start();
        }

    }
    static class Child extends Thread{
        private CyclicBarrier cyclicBarrier;
        private String name;

        public Child(CyclicBarrier cyclicBarrier, String name) {
            this.cyclicBarrier = cyclicBarrier;
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(this.name + "正在洗手...");
            try {
                Thread.sleep(5000);      //以睡眠來模擬洗手
                System.out.println(this.name +"洗好了,等待其他小朋友洗完...");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有小朋友都洗好手了,開始喫飯吧...");
            try {
                Thread.sleep(5000);      //以睡眠來模擬喫飯
                System.out.println(this.name+"喫好了,等待其他小朋友喫完.....");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有小朋友喫好了,一起去玩吧...");
        }
    }
}

執行結果:

小明正在洗手...
小紅正在洗手...
小亮正在洗手...
小紅洗好了,等待其他小朋友洗完...
小明洗好了,等待其他小朋友洗完...
小亮洗好了,等待其他小朋友洗完...
所有小朋友都洗好手了,開始喫飯吧...
所有小朋友都洗好手了,開始喫飯吧...
所有小朋友都洗好手了,開始喫飯吧...
小紅喫好了,等待其他小朋友喫完.....
小亮喫好了,等待其他小朋友喫完.....
小明喫好了,等待其他小朋友喫完.....
所有小朋友喫好了,一起去玩吧...
所有小朋友喫好了,一起去玩吧...
所有小朋友喫好了,一起去玩吧...

CyclicBarrier類位於java.util.concurrent包下,CyclicBarrier提供2個構造器:

public CyclicBarrier(int parties) {

}
public CyclicBarrier(int parties, Runnable barrierAction) {

}

上面的例子中我們用到了第一個,下面的構造器多了一個參數Runable對象,當所有線程到達該屏障時執行該Runable對象。

參數parties指讓多少個線程或者任務等待至barrier狀態;參數barrierAction爲當這些線程都達到barrier狀態時會執行的內容,當所有線程到達該屏障時執行該Runable對象。比如下面的代碼中,傳入Runnable對象,當所有線程都到達barrier狀態時,會執行該Runable對象的run方法。

package com.example.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier barrier  = new CyclicBarrier(3,new Runnable() {
            @Override
            public void run() {
                System.out.println("開始做下一件事吧...");
            }});
        List<Thread> threads = new ArrayList<>(3);
        threads.add(new Thread(new Child(barrier, "小明")));
        threads.add(new Thread(new Child(barrier, "小紅")));
        threads.add(new Thread(new Child(barrier, "小亮")));

        for (Thread thread : threads) {
            thread.start();
        }

    }
    static class Child extends Thread{
        private CyclicBarrier cyclicBarrier;
        private String name;

        public Child(CyclicBarrier cyclicBarrier, String name) {
            this.cyclicBarrier = cyclicBarrier;
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(this.name + "正在洗手...");
            try {
                Thread.sleep(5000);      //以睡眠來模擬洗手
                System.out.println(this.name +"洗好了,等待其他小朋友洗完...");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            //System.out.println("所有小朋友都洗好手了,開始喫飯吧...");
            try {
                Thread.sleep(5000);      //以睡眠來模擬喫飯
                System.out.println(this.name+"喫好了,等待其他小朋友喫完.....");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            //System.out.println("所有小朋友喫好了,一起去玩吧...");
        }
    }
}

結果:

小明正在洗手...
小紅正在洗手...
小亮正在洗手...
小明洗好了,等待其他小朋友洗完...
小紅洗好了,等待其他小朋友洗完...
小亮洗好了,等待其他小朋友洗完...
開始做下一件事吧...
小亮喫好了,等待其他小朋友喫完.....
小明喫好了,等待其他小朋友喫完.....
小紅喫好了,等待其他小朋友喫完.....
開始做下一件事吧...

CyclicBarrier有哪些常用的方法?

從上面的例子我們也可以看到,await是其最重要的方法。它有2個重載版本:

public int await() throws InterruptedException, BrokenBarrierException { };
public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { };

第一個版本比較常用,用來掛起當前線程,直至所有線程都到達barrier狀態再同時執行後續任務;第二個版本是讓這些線程等待至一定的時間,如果還有線程沒有到達barrier狀態就直接讓到達barrier的線程執行後續任務。

比如:

package com.example.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier barrier  = new CyclicBarrier(3,new Runnable() {
            @Override
            public void run() {
                System.out.println("開始做下一件事吧...");
            }});
        List<Thread> threads = new ArrayList<>(3);
        threads.add(new Thread(new Child(barrier, "小明")));
        threads.add(new Thread(new Child(barrier, "小紅")));
        threads.add(new Thread(new Child(barrier, "小亮")));

        for (Thread thread : threads) {
            thread.start();
        }

    }
    static class Child extends Thread{
        private CyclicBarrier cyclicBarrier;
        private String name;

        public Child(CyclicBarrier cyclicBarrier, String name) {
            this.cyclicBarrier = cyclicBarrier;
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(this.name + "正在洗手...");
            try {
                if(this.name.equalsIgnoreCase("小亮")){
                    Thread.sleep(8000);
                }else{
                    Thread.sleep(5000);
                }//以睡眠來模擬洗手
                System.out.println(this.name +"洗好了,等待其他小朋友洗完...");
                cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

結果:

小明正在洗手...
小紅正在洗手...
小亮正在洗手...
小明洗好了,等待其他小朋友洗完...
小紅洗好了,等待其他小朋友洗完...
java.util.concurrent.TimeoutException
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:257)
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
	at com.example.demo.Test$Child.run(Test.java:46)
	at java.lang.Thread.run(Thread.java:748)
java.util.concurrent.BrokenBarrierException
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
	at com.example.demo.Test$Child.run(Test.java:46)
	at java.lang.Thread.run(Thread.java:748)
小亮洗好了,等待其他小朋友洗完...
java.util.concurrent.BrokenBarrierException
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
	at com.example.demo.Test$Child.run(Test.java:46)
	at java.lang.Thread.run(Thread.java:748)

上面的例子中,我們故意讓小亮延遲了3000ms,而await的超時時間是2000ms,所以小亮在執行的時候就會拋異常,繼續執行後面的任務。

CyclicBarrier實現柵欄原理?

在CyclicBarrier的內部定義了一個ReentrantLock的對象,然後再利用這個ReentrantLock對象生成一個Condition的對象。每當一個線程調用CyclicBarrier的await方法時,首先把剩餘屏障的線程數減1,然後判斷剩餘屏障數是否爲0:如果不是,利用Condition的await方法阻塞當前線程;如果是,首先利用Condition的signalAll方法喚醒所有線程,最後重新生成Generation對象以實現屏障的循環使用。
 

CountDownLatch

CountDownLatch是JDK提供的一個同步工具,CyclicBarrier類也位於java.util.concurrent包下。它的作用是讓一個或多個線程等待,一直等到其他線程中執行完成一組操作。

比如遊戲英雄聯盟,主線程爲控制遊戲開始的線程,其他線程爲遊戲玩家。在所有的玩家都準備好之前,主線程是處於等待狀態的,也就是遊戲不能開始。當所有的玩家準備好之後,主線程才能開始遊戲。這時候可以用到CountDownLatch。

CountDownLatchTest.java

package com.example.demo;

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        for(int i = 0; i < latch.getCount(); i++){
            new Thread(new MyThread(latch), "player"+i).start();
        }
        System.out.println("正在等待所有玩家準備好");
        latch.await();
        System.out.println("開始遊戲");
    }

    private static class MyThread implements Runnable{
        private CountDownLatch latch ;

        public MyThread(CountDownLatch latch){
            this.latch = latch;
        }

        @Override
        public void run() {
            try {
                Random rand = new Random();
                int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;//產生1000到3000之間的隨機整數
                Thread.sleep(randomNum);
                System.out.println(Thread.currentThread().getName()+" 已經準備好了, 所使用的時間爲 "+((double)randomNum/1000)+"s");
                latch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

在上面的代碼中,我們模擬5個玩家,只有在5個玩家都準備好之後,才能開始遊戲。

結果:

正在等待所有玩家準備好
player4 已經準備好了, 所使用的時間爲 1.801s
player3 已經準備好了, 所使用的時間爲 1.835s
player2 已經準備好了, 所使用的時間爲 2.439s
player0 已經準備好了, 所使用的時間爲 2.763s
player1 已經準備好了, 所使用的時間爲 2.816s
開始遊戲

CountDownLatch有哪些常用的方法?

countDown方法和await方法。CountDownLatch在初始化時,需要指定用給定一個整數作爲計數器。當調用countDown方法時,計數器會被減1;當調用await方法時,如果計數器大於0時,線程會被阻塞,一直到計數器被countDown方法減到0時,線程纔會繼續執行。計數器是無法重置的,當計數器被減到0時,調用await方法都會直接返回。

其中await方法同CyclicBarrier一樣,有兩個重載版本。

public void await() throws InterruptedException { };   //調用await()方法的線程會被掛起,它會等待直到count值爲0才繼續執行

public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  //和await()類似,只不過等待一定的時間後count值還沒變爲0的話就會繼續執行

下面看下await帶有timeout參數的用法:

package com.example.demo;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        for(int i = 0; i < latch.getCount(); i++){
            new Thread(new MyThread(latch,"player"+i), "player"+i).start();
        }
        System.out.println("正在等待所有玩家準備好");
        latch.await(3, TimeUnit.SECONDS);
        System.out.println("開始遊戲");
    }

    private static class MyThread implements Runnable{
        private CountDownLatch latch ;
        private String name ;

        public MyThread(CountDownLatch latch){
            this.latch = latch;
        }

        public MyThread(CountDownLatch latch, String name){
            this.latch = latch;
            this.name = name;
        }

        @Override
        public void run() {
            try {
                Random rand = new Random();

                int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;//產生1000到3000之間的隨機整數
                if(("player2").equalsIgnoreCase(this.name)){
                    randomNum = 4000;
                }
                Thread.sleep(randomNum);
                System.out.println(Thread.currentThread().getName()+" 已經準備好了, 所使用的時間爲 "+((double)randomNum/1000)+"s");
                latch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

上面的代碼中latch.await(3, TimeUnit.SECONDS);設置主程序等待時間爲3秒,而針對palyer2玩家設置準備時間4秒,延遲了1秒。

運行結果:

正在等待所有玩家準備好
player4 已經準備好了, 所使用的時間爲 1.994s
player0 已經準備好了, 所使用的時間爲 2.006s
player1 已經準備好了, 所使用的時間爲 2.119s
player3 已經準備好了, 所使用的時間爲 2.895s
開始遊戲
player2 已經準備好了, 所使用的時間爲 4.0s

我們可以看到主程序沒有等待palyer2玩家,直接開始了遊戲。

CountDownLatch的實現原理是什麼?

CountDownLatch有一個內部類叫做Sync,它繼承了AbstractQueuedSynchronizer類,其中維護了一個整數state,並且保證了修改state的可見性和原子性。state使用volatile修飾,保證其可見性。
在countDown方法中,調用Sync實例的releaseShared方法,在該方法中,先獲取當前當前計數器的值,如果計數器爲0,則直接喚醒所有被await阻塞的線程,如果計數器不爲0, 先利用CAS對計數器進行減1,如果減1後的計數器爲0,則直接喚醒所有被await阻塞的線程。state減一操作利用CAS原理,保證其原子性。在await方法中,判斷計數器是否爲0,如果不爲0,則阻塞當前線程。

 

乍一看CountDownLatch和CyclicBarrier的功能很相似:從字面上理解,CountDown表示減法計數,Latch表示門閂的意思,計數爲0的時候就可以打開門閂了。Cyclic Barrier表示循環的障礙物。兩個類都含有這一個意思:對應的線程都完成工作之後再進行下一步動作,也就是大家都準備好之後再進行下一步。然而兩者最大的區別是,進行下一步動作的動作實施者是不一樣的。這裏的“動作實施者”可以看做兩種,一種是主線程(即執行main函數,此處說主線程不太準確,更確切的說法應該是調用countDownLatch.await的線程),另一種是執行任務的其他線程,後面叫這種線程爲“其他線程”,區別於主線程。對於CountDownLatch,當計數爲0的時候,下一步的動作實施者是main函數;對於CyclicBarrier,下一步動作實施者是“其他線程”。

比如上面CountDownLatch的例子,當CountDownLatch計數爲0的時候,下一步的動作實施者是main函數,執行的動作是開始遊戲。而上面CyclicBarrier,當所有小朋友線程都到達Barrier狀態時,下一步的動作實施者仍然是小朋友線程,此例中是等所有小朋友洗好手後,小朋友們再一起喫飯。

Semaphore

Semaphore翻譯成字面意思爲信號量,它通過維護若干個許可證來控制線程對共享資源的訪問。如果許可證剩餘數量大於零時,線程則允許訪問該共享資源,如果許可證剩餘數量爲零,則拒絕線程訪問該共享資源。Semaphore所維護的許可證數量就是允許訪問共享資源的最大線程數量。線程想要訪問共享資源必須從Semaphore中獲取到許可證。

舉個例子:

比如小明、小紅、小亮、小蘭一起去飯店喫飯,而飯店衛生間只有2個洗手池,這種場景共享資源就是洗手池。可以用到Semaphore。

SemaphoreTest.java

package com.example.demo;


import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Semaphore;

public class SemaphoreTest {
    public static void main(String[] args) throws InterruptedException {
        //飯店裏只用兩個洗手池,所以初始化許可證的總數爲2。
        Semaphore washbasin = new Semaphore(2);

        List<Thread> threads = new ArrayList<>(3);
        threads.add(new Thread(new Customer(washbasin, "小明")));
        threads.add(new Thread(new Customer(washbasin, "小紅")));
        threads.add(new Thread(new Customer(washbasin, "小亮")));
        threads.add(new Thread(new Customer(washbasin, "小蘭")));
        for (Thread thread : threads) {
            thread.start();
            Thread.sleep(50);
        }

        for (Thread thread : threads) {
            thread.join();
        }
    }

    static class Customer implements Runnable {
        private Semaphore washbasin;
        private String name;

        public Customer(Semaphore washbasin, String name) {
            this.washbasin = washbasin;
            this.name = name;
        }

        @Override
        public void run() {
            try {
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
                Random random = new Random();

                washbasin.acquire();
                System.out.println(
                    sdf.format(new Date()) + " " + name + " 開始洗手...");
                Thread.sleep((long) (random.nextDouble() * 5000) + 2000);
                System.out.println(
                    sdf.format(new Date()) + " " + name + " 洗手完畢!");
                washbasin.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

結果:

16:09:52.322 小明 開始洗手...
16:09:52.339 小紅 開始洗手...
16:09:54.742 小明 洗手完畢!
16:09:54.742 小亮 開始洗手...
16:09:57.783 小紅 洗手完畢!
16:09:57.783 小蘭 開始洗手...
16:10:00.196 小亮 洗手完畢!
16:10:00.397 小蘭 洗手完畢!

可以看到同一時刻最多有2個人在洗手。

Semaphore有哪些常用的方法?

public void acquire() throws InterruptedException {  }     //獲取一個許可
public void acquire(int permits) throws InterruptedException { }    //獲取permits個許可
public void release() { }          //釋放一個許可
public void release(int permits) { }    //釋放permits個許可

最主要的是acquire方法和release方法。 當調用acquire方法時線程就會被阻塞,直到Semaphore中可以獲得到許可證爲止。 當調用release方法時將向Semaphore中添加一個許可證,如果有線程因爲獲取許可證被阻塞時,它將獲取到許可證並被釋放;

這4個方法都會被阻塞,如果想立即得到執行結果,可以使用下面幾個方法:

public boolean tryAcquire() { };    //嘗試獲取一個許可,若獲取成功,則立即返回true,若獲取失敗,則立即返回false
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };  //嘗試獲取一個許可,若在指定的時間內獲取成功,則立即返回true,否則則立即返回false
public boolean tryAcquire(int permits) { }; //嘗試獲取permits個許可,若獲取成功,則立即返回true,若獲取失敗,則立即返回false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //嘗試獲取permits個許可,若在指定的時間內獲取成功,則立即返回true,否則則立即返回false

Semaphore的內部原理?

Semaphore內部主要通過AQS(AbstractQueuedSynchronizer)實現線程的管理。Semaphore在構造時,需要傳入許可證的數量,它最後傳遞給了AQS的state值。線程在調用acquire方法獲取許可證時,如果Semaphore中許可證的數量大於0,許可證的數量就減1,線程繼續運行,當線程運行結束調用release方法時釋放許可證時,許可證的數量就加1。如果獲取許可證時,Semaphore中許可證的數量爲0,則獲取失敗,線程進入AQS的等待隊列中,等待被其它釋放許可證的線程喚醒。

上面的例子中,這4個人會按照線程啓動的順序洗手嘛?

不一定。

Semaphore類位於java.util.concurrent包下,它提供了2個構造器:

public Semaphore(int permits) {          //參數permits表示許可數目,即同時可以允許多少線程進行訪問
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {    //這個多了一個參數fair表示是否是公平的,即等待時間越久的越先獲取許可
    sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}

第二個構造器中,它提供了一個fair參數,表示是否公平。默認是false,即NonfairSync(非公平鎖),這個類不保證線程獲得許可證的順序,調用acquire方法的線程可能在一直等待的線程之前獲得一個許可證。如果fair參數傳入true,這樣使用的是FairSync(公平鎖),可以確保按照各個線程調用acquire方法的順序獲得許可證。

非公平鎖與公平鎖的區別是?

對於非公平鎖,當一個線程A調用acquire方法時,會直接嘗試獲取許可證,而不管同一時刻阻塞隊列中是否有線程也在等待許可證,如果恰好有線程C調用release方法釋放許可證,並喚醒阻塞隊列中第一個等待的線程B,此時線程A和線程B是共同競爭可用許可證,不公平性就體現在:線程A沒任何等待就和線程B一起競爭許可證了。

和非公平策略相比,FairSync中多一個對阻塞隊列是否有等待的線程的檢查,如果沒有,就可以參與許可證的競爭;如果有,線程直接被插入到阻塞隊列尾節點並掛起,等待被喚醒。

 

參考:

https://www.cnblogs.com/dolphin0520/p/3920397.html

https://blog.csdn.net/liangyihuai/article/details/83106584

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