Java併發輔助類

序言

由於最近項目上遇到了高併發問題,而自己對高併發,多線程這裏的知識點相對薄弱,尤其是基礎,所以想系統的學習一下,以後可能會出一系列的JUC文章及總結 ,同時也爲企業級的高併發項目做好準備。

本文是JUC文章的第五篇,如想看以往關於JUC文章,請點擊JUC系列總結

此係列文章的總結思路大致分爲三部分:

  1. 理論(概念);
  2. 實踐(代碼證明);
  3. 總結(心得及適用場景)。

在這裏提前說也是爲了防止大家看着看着就迷路了。

備註:本文的知識深度相對較淺,可能僅侷限於應用層面,如您需要相應足夠的深度,請另行查閱。

Java併發輔助類大綱

CountDownLatch

Java輔助類.png

什麼是CountDownLatch呢?

CountDownLatch:一個線程(或者多個線程),等待另外N個線程完成某個事情之後,方可執行。

可能大家看完之後還是雲裏霧裏的,我還是給大家舉一個實際場景的例子把。

模擬場景:

  1. 教室裏有7個人上自習,分爲爲一個班長和其他6位同學;

  2. 現在的要求是班長必須最後一個走,因爲他是班長,所以一定要確保走後教室燈關掉,門鎖上;

  3. 其他6位同學隨時可以走;

常用方法

  • CountDownLatch(int count); //構造方法,創建一個值爲count 的計數器。
  • await();//阻塞當前線程,將當前線程加入阻塞隊列。
  • await(long timeout, TimeUnit unit);//在timeout的時間之內阻塞當前線程,時間一過則當前線程可以執行,
  • countDown();//對計數器進行遞減1操作,當計數器遞減至0時,當前線程會去喚醒阻塞隊列裏的所有線程。

代碼證明

public class CountDownLatchTest {
    public static void main(String[] args) {
        testCase();
}
    public static void testCase() {
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i < 6; i++) {
            new Thread(() ->{
                System.out.println(Thread.currentThread().getName()+"同學\t 離開了教室");
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        try {
            countDownLatch.await();
            System.out.println("班長最後走,並把教室燈關了,門鎖住====");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出如下:

0同學	 離開了教室
3同學	 離開了教室
2同學	 離開了教室
1同學	 離開了教室
4同學	 離開了教室
5同學	 離開了教室
班長最後走,並把教室燈關了,門鎖住====

CountDownLatch結合枚舉的使用

現在模擬一個場景:秦始皇一統天下的過程。

  1. 首先,春秋時代,一個有六個國家,所以你給的參數不能在6之外;
  2. 六個國家先後被滅(此處忽略被滅順序);
  3. 等六個國家全部滅了之後,秦始皇才一統天下,不能‘某一個國家被滅’出現在‘秦始皇已經一統天下’之後;

上代碼:

枚舉類:

public enum CountryEnum {
    ONE(1,"齊"),TWO(2,"楚"),TRHEE(3,"燕"),
    FOUR(4,"趙"),FIVE(5,"魏"),SIX(6,"韓");

    private Integer code;
    private String country;
	//私有構造
    CountryEnum(Integer code, String country) {
        this.code = code;
        this.country = country;
    }
	//對外公開的方法,用於匹配
    public static String getCountryByCode(Integer code){
        CountryEnum[] countryValues = CountryEnum.values();
        for(CountryEnum country:countryValues){
            if(code == country.getCode()){
                return country.getCountry();
            }
        }
        return null;
    }
	getGetter/Setter方法略...
}

測試類:

public class CountDownLatchTest {
    public static void main(String[] args) {
        actualUse();
    }

    public static void actualUse() {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() ->{
                System.out.println(Thread.currentThread().getName()+"國\t 被滅了...");
                countDownLatch.countDown();
            },CountryEnum.getCountryByCode(i)).start();
        }
        try {
            countDownLatch.await();
            System.out.println("秦國一統天下======");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出如下:

齊國	 被滅了...
魏國	 被滅了...
韓國	 被滅了...
燕國	 被滅了...
趙國	 被滅了...
楚國	 被滅了...
秦國一統天下======

結合枚舉使用的好處:

其實我們可以反過來推,如果這種情況不使用枚舉,他會怎麼寫?

肯定是在線程內部去判斷code值,類似這種:

if(i == 1){
	System.out.println("齊國被滅了...")
}else if(i == 1){
	System.out.println("楚國被滅了...")
}else if(){
...
}

這種問題在5個線程下還好,但如果50個呢,500個呢,這樣就很難維護。

枚舉不僅可以簡化我們的代碼,降低維護成本,而且還可以限制傳參,比如,在當前代碼裏,你傳一個7,他肯定是錯誤的。

綜上所述,使用枚舉的好處:

  1. 簡化代碼,降低後期維護成本;
  2. 限制傳參類型,防止部分惡意傳參。

CountDownLatch原理

由於這裏本人對AQS的原理暫時還不清楚,所以這裏不做詳細展示,

後期待詳細瞭解之後,再來做詳細補充…

本文的原理這塊都是總結性的東西,如想詳細研究,你可以通過其他資料查閱。

CountDownLatch是通過維護一個計數器,然後通過AQS去實現阻塞與喚醒機制。

當我們調用CountDownLatch countDownLatch = new CountDownLatch(N)的時候,他會將這個N傳遞給AQS隊列的state,而這個state的值代表CountDownLatch所剩餘的計數次數。

 public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);//創建同步隊列,並設置初始計數器值(Sync爲AQs的實現)
    }

調用await()的時候,會創建一個節點,加入到AQS阻塞隊列,並同時把當前線程掛起。

public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

調用countDownLatch.down()的時候,會對計數器進行減一操作,一旦爲零,則喚醒所有阻塞隊列裏面的線程。

CyclicBarrier

什麼是CyclicBarrier呢?

CyclicBarrierN個線程相互等待,任何一個線程在未完成之前,所有的線程都必須等待。

還是老樣子,舉個通俗易懂的例子把。

模擬場景:待收集完七顆龍珠,即可召喚神龍,缺任何一顆,都不行。

常用方法

//構造方法1
public CyclicBarrier(int parties) {
    this(parties, null);
}
 //構造方法2
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}
//await方法
public int await() throws InterruptedException, BrokenBarrierException {
    try {
        // 不超時等待
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}

代碼證明

public class CyclicbarrierTest {

    public static final Integer CYCLICBARRIER_SIZE = 7;

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(CYCLICBARRIER_SIZE,()->{
            System.err.println("七顆龍珠已集齊,出來吧,神龍!");
        });
        for (int i = 1; i <= 7; i++) {
            new Thread(() ->{
                try {
                    System.out.println("第"+Thread.currentThread().getName()+"顆龍珠已集齊!");
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

輸出結果:

1顆龍珠已集齊!
第5顆龍珠已集齊!
第4顆龍珠已集齊!
第7顆龍珠已集齊!
第2顆龍珠已集齊!
第3顆龍珠已集齊!
第6顆龍珠已集齊!
七顆龍珠已集齊,出來吧,神龍!

CyclicBarrier原理

本文的原理這塊都是總結性的東西,如想詳細研究,你可以通過其他資料查閱。

基於ReentrantLock和Condition來實現的。

Semaphore

什麼是semaphore呢?

semaphroe:信號量,一種共享鎖,指的是控制某個資源被訪問的線程數

模擬場景:

  1. 停車場共有3個車位,在同一時間內,有且僅能停3輛車;
  2. 超出3輛車的部分則阻塞,即等待;
  3. 當3個車位中有車駛出時,下面等待的車 則進來使用。

常用方法

acquire() / acquire(int permits) 
獲取1/多個憑證,如果憑證數量不夠,等待其他線程釋放(在未獲取到憑證之前、或者被其他線程調用中斷之前,該線程一直處於阻塞狀態。)
注意:此處的permits代表一個線程要佔用2個憑證,如果總憑證爲10,permits=2,只能同時5個線程佔用。
​    
acquireUninterruptibly() 
獲取一個憑證,在獲取到憑證之前線程一直處於阻塞狀態(忽略中斷)。
    
tryAcquire() / tryAcquire(int permits)
嘗試獲得憑證,返回獲取憑證成功或失敗,不阻塞線程。
​
tryAcquire(long timeout, TimeUnit unit)
嘗試獲得憑證,在超時時間內循環嘗試獲取,限時等待。
​
release() / release(int permits)
釋放1/多個憑證,喚醒1/多個獲取憑證不成功的阻塞線程。
​
hasQueuedThreads()
等待隊列裏是否還存在等待線程。
​
getQueueLength()
獲取等待隊列裏阻塞的線程數。
​
drainPermits()
清空憑證把可用憑證數置爲0,返回清空憑證的數量。
​
availablePermits()
返回可用的憑證數量。

代碼證明

public class SemaphoreTest {
    public static final int SEMAPHORE_SIZE = 4;

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(SEMAPHORE_SIZE);
        for (int i = 1; i <= 8; i++) {
            new Thread(() ->{
                try {
                    System.out.println(Thread.currentThread().getName()+"車\t 來到了停車場 ");
                    if(semaphore.availablePermits()==0){
                        System.out.println("車位不足,請耐心等待");
                    }
                    semaphore.acquire();
                    System.err.println(Thread.currentThread().getName()+"車\t搶到了車位");
                    try{ TimeUnit.SECONDS.sleep(3);}catch(Exception e){e.getStackTrace();};
                    System.out.println(Thread.currentThread().getName()+"車\t休息了3秒,開走了===");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally{
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

輸出結果:

1車	 來到了停車場 
5車	 來到了停車場 
6車	 來到了停車場 
4車	 來到了停車場 
2車	 來到了停車場 
車位不足,請耐心等待
3車	 來到了停車場 
車位不足,請耐心等待
8車	 來到了停車場 
車位不足,請耐心等待
7車	 來到了停車場 
車位不足,請耐心等待
1車	搶到了車位
5車	搶到了車位
6車	搶到了車位
4車	搶到了車位
1車	休息了3秒,開走了===
4車	休息了3秒,開走了===
5車	休息了3秒,開走了===
6車	休息了3秒,開走了===
2車	搶到了車位
8車	搶到了車位
7車	搶到了車位
3車	搶到了車位
2車	休息了3秒,開走了===
7車	休息了3秒,開走了===
8車	休息了3秒,開走了===
3車	休息了3秒,開走了===

Semapore原理

由於這裏本人對AQS的原理暫時還不清楚,所以這裏不做詳細展示,

後期待詳細瞭解之後,再來做詳細補充…

本文的原理這塊都是總結性的東西,如想詳細研究,你可以通過其他資料查閱。

跟CountDownLatch一樣,也是維護了一個繼承了AQS的Sync同步器,對線程的控制均通過sync來實現。

初始化時 Semaphore semaphore=new Semaphore(n)

默認一個非公平鎖的同步阻塞隊列,他會將這個N傳遞給AQS隊列的state,代表我要申請憑證的數量。

public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

acquire/realease則是線程的喚醒與阻塞機制…

拓展

CyclicBarrier與CountDownLatch的區別

  1. 對於CountDownLatch來說,他的側重點在於一個線程在等待,而另外那N的線程在把**“某個事情”**做完之後可以繼續等待,可以終止。
    對於CyclicBarrier來說,他的側重點是在於N個線程,如果其中任何一個沒有完成,所有的線程都必須等待。
  2. CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能夠處理更爲複雜的場景;

另:在semaphore與countDownLatch的比較中,前者可重複利用,就跟停車一樣,只要資源還在,就可以重複,但countDownLatch不可以,當count減完之後,不可再利用。

總結

其實本文只是一個簡單的使用,對於涉及到原理部分,由於暫時對AQS瞭解不深,不敢妄加評論。

Renference

關於java多線程淺析六: CyclicBarrier的原理分析和使用

CountDownLatch的使用和原理解析

Semaphore 使用及原理

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