【并發編程系列7】CountDownLatch,CyclicBarrier,Semaphore實現原理分析

CountDownLatch

CountDownLatch是一個同步工具類,它允許一個或多個線程一直等待,直到其他線程的操作執行完畢再執行。

CountDownLatch使用示例

package com.zwx.concurrent.jucUtil;

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        new Thread(()->{
            System.out.println("----------");
            countDownLatch.countDown();
        }).start();
        countDownLatch.await();
        System.out.println("==========");
    }
}

CountDownLatch的構造函數接收一個int類型的參數作為計數器,可以視自己的需求設置一個合法的int數值,執行一次countDown()方法,計數器就會減1,await()方法會阻塞線程,直到線程計數減少為0才會繼續運行。如果說為了防止某一個線程卡死導致await()一直阻塞的話,也可以調用await(long timeout, TimeUnit unit)方法設定超時時間,到達超時時間之后,即使計數器不到0,也可以繼續執行后面的代碼。

CountDownLatch源碼分析

CountDownLatch(count)

這個方法比較簡單,就是維護了一個計數器:


在這里插入圖片描述

我們可以看到,調用了Sync類的構造器,而Sync繼承了AbstractQueuedSynchronizer類,最終實際上是設置到AbstractQueuedSynchronizer中state屬性上,state屬性我們前面的AQS文章中提到過,0表示無鎖狀態,>=1之后表示加鎖次數,所以這里的計數器就相當于加鎖了N次。


在這里插入圖片描述

CountDownLatch#await()

進入await()方法


在這里插入圖片描述

然后繼續調用了sync的acquireSharedInterruptibly(arg)方法:


如果被中斷

1325行的if判斷就是判斷當前計數器state是不是等于0了,最終調用的是CountDownLatch中的內部類Sync的tryAcquireShared(arg)方法:


在這里插入圖片描述

如果state!=0,那就返回-1,返回-1之后需要阻塞線程,所以就會繼續執行之后的方法doAcquireSharedInterruptibly(arg)方法。

AQS#doAcquireSharedInterruptibly(arg)

在這里插入圖片描述

注意一下999行,這里也是將當前線程封裝成一個節點,并構建一個AQS同步隊列,前面我們分析重入鎖的時候提到了AQS有兩種功能,一個是獨占,一個就是共享,前面講過的ReentrantLock中就是以獨占模式封裝的Node,而這里是以共享模式構建。
共享模式和獨占模式在對象中表現出來的區別我們可以進入Node類看一下:


在這里插入圖片描述
在這里插入圖片描述

所以獨占和共享模式構建的節點唯一區別就是共享節點中的nextWaiter不為空(另外還有Condition隊列中的nextWaiter也不為空)。

這個方法中前面的一些邏輯AQS中分析過來,這里就不重復分析,這時候我們進來r>=0肯定是不成立的,所以會走到后面的線程掛起,掛起之后線程就阻塞了,那么阻塞了就一定需要被喚醒,所以我們猜測上文示例中的countDown()不但是將計數器減1,肯定還會有判斷當減少到0的時候需要喚醒線程。

CountDownLatch#countDown()

調用之后進入CountDownLatch

調用的是sync類中的方法releaseShared(arg),注意這里固定傳的是1,因為調用一次countDown()方法計數減1。

AQS#releaseShared(arg)

在這里插入圖片描述

這里做了一個if判斷,嘗試是否可以釋放,如果可以釋放之后再執行釋放,我們進入tryReleaseShared(arg)方法中一窺究竟。

CountDownLatch#tryReleaseShared(releases)

在這里插入圖片描述

注意上面是一個死循環,只有兩種情況可以跳出循環,一種就是當前state已經等于0,另一種就是CAS成功,也就是說減1成功。
如果返回false,就說明還需要阻塞等待其他線程;如果返回的是true,就會直接后面的doReleaseShared()方法。

AQS#doReleaseShared()

這個方法主要就是通過一個循環將head節點喚醒,因為中途可能會被其他線程喚醒了或者也可能加入了新節點,所以需要通過一個死循環來確保釋放成功


在這里插入圖片描述

回到AQS#doAcquireSharedInterruptibly(arg)

在這里插入圖片描述

上面await()方法的線程阻塞在1014這個if條件這里,喚醒之后如果沒有被中斷過,那么會繼續執行for循環,這時候r>=0肯定成立了,所以會進入setHeadAndPropagate(Node,int)方法,去依次傳播所有需要喚醒的節點

AQS#setHeadAndPropagate(Node,int)

在這里插入圖片描述

這里注意到參數中的Node是head節點的下一個節點,所以這里要做的是把第二個節點替換成Node節點,然后執行同一個方法doReleaseShared()方法去喚醒頭節點,喚醒之后會回到上面的for循環,繼續喚醒后一個節點,直到全部線程均被喚醒。

CyclicBarrier

CyclicBarrier的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一 組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最后一個線程到達屏障時,屏障才會 開門,所有被屏障攔截的線程才會繼續運行。CyclicBarrier可以用于多線程計算數據,最后合并計算結果的場景

CyclicBarrier使用示例1

package com.zwx.concurrent.jucUtil;

import java.util.concurrent.CyclicBarrier;

public class CyclicbarrierDemo {
    static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
    public static void main(String[] args) {
        new Thread(()-> {
            try {
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("我是線程t1");
        },"t1").start();
        new Thread(()-> {
            try {
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("我是線程t2");
        },"t2").start();

        System.out.println("主線程==end");
    }
}

輸出結果:


在這里插入圖片描述

這個t1和t2的輸出結果是隨機的。

CyclicBarrier還提供一個更高級的構造函數CyclicBarrier(int parties,Runnable barrier- Action),用于在線程到達屏障時,優先執行barrierAction。

CyclicBarrier使用示例2

package com.zwx.concurrent.jucUtil;

import java.util.concurrent.CyclicBarrier;

public class CyclicbarrierDemo2 {
    static CyclicBarrier cyclicBarrier = new CyclicBarrier(2,new MyThread());
    public static void main(String[] args) {
        new Thread(()-> {
            try {
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("我是線程t1");
        },"t1").start();
        new Thread(()-> {
            try {
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("我是線程t2");
        },"t2").start();

        System.out.println("主線程==end");
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("do something");
        }
    }
}

輸出結果:


在這里插入圖片描述

我們可以看到,在線程t1和t2輸出前會先輸出自定義線程的信息。

CyclicBarrier實現原理

CyclicBarrier 相比 CountDownLatch 來說,要簡單很多,源碼實現是基于 ReentrantLock 和 Condition 的組合使用

CyclicBarrier源碼分析

CyclicBarrier(parties)

進入CyclicBarrier默認構造器:


在這里插入圖片描述

可以發現,最終其實還是調用的CyclicBarrier(int parties,Runnable barrier- Action)構造器:


在這里插入圖片描述

注意了,構造CyclicBarrier對象時,初始化了多少個parties,則必須對應有parties個線程調用await()方法,否則線程不會往后執行。

CyclicBarrier#await()

在這里插入圖片描述

調用了dowait(timed,nanos)方法,第一個參數false表示未設置超時時間,后面表示納秒數,因為await還有另一個對應的方法帶上超時時間:await(long,timeunit),這個方法中調用dowait(timed,nanos)方法時第一個參數就會是true,然后帶上超時時間,表示到了設定時間之后線程就不會被阻塞,會繼續往后執行。

CyclicBarrier#dowait()

/**
     * Main barrier code, covering the various policies.
     * 主要屏障代碼,覆蓋了各種策略
     */
    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;//定義一個重入鎖:private final ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try {
            //同一個屏障初始進來時屬于同一代或者說一個周期,構建一個"代"(Generation)對象,同一個Generation表示同一代
            final Generation g = generation;//Generation中設置了broke=false,表示屏障沒有損壞

            if (g.broken)//如果broken=true表示當前屏障被損壞了,拋出異常
                throw new BrokenBarrierException();

            if (Thread.interrupted()) {//如果線程被中斷過
                breakBarrier();//設置屏障為損壞狀態并喚醒所有持有鎖的線程
                throw new InterruptedException();//拋出中斷異常
            }

            int index = --count;//未調用await()方法的線程計數-1
            if (index == 0) {//如果屏障數為0,(表示所有線程都到達await()方法)
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();//表示到達屏障之后,如果我們有設置barrierCommand,則優先執行
                    ranAction = true;
                    //執行到這里的時候,說明所有線程都到了await()方法,且設置的barrierCommand也已經執行完了
                    //接下來要做的事情就是換代(所以CyclicBarrier是通過換代的方式實現重新計數的)
                    //換代之后相當于進入一個新的周期,所有線程在后續中又可以通過await()阻塞一次
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)//如果ranAction = false說明當前屏障還有流程沒執行完,所以需要屏障設置會損壞狀態
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            //死循環等到count=0,調用breakBarrier方法(表示屏障有問題的場景),中斷或者超時
            for (;;) {
                try {
                    if (!timed)
                        //private final Condition trip = lock.newCondition();
                        trip.await();//即Condition隊列的await()阻塞,相當于把線程加入到Condition隊列中阻塞
                    else if (nanos > 0L)//超時時間大于0
                        nanos = trip.awaitNanos(nanos);//阻塞指定時間
                } catch (InterruptedException ie) {
                    //如果當前屏障沒有換代,也沒有損壞,那么就設置為損壞狀態之后再拋出中斷異常
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)//如果屏障已經被損壞了
                    throw new BrokenBarrierException();

                if (g != generation)//如果發現已經換代了,就不繼續循環了,直接返回就好了
                    return index;//返回當前還有多少個線程沒有執行await()方法

                if (timed && nanos <= 0L) {//表示超時時間到了
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

這里的方法看起來很長,但其實除了一系列的判斷,并沒有多代碼,結合注釋,如果前面理解了ReentrantLock和Condition隊列的話,應該非常好看懂,里面調用的其他一些子方法這里也不做單獨介紹。

使用CyclicBarrier注意事項

1、對于指定計數值 parties,若由于某種原因,沒有足夠的線程調用 CyclicBarrier 的await()方法,則所有調用 await 的線程都會被阻塞;
2、若有多余線程執行了await()方法,那么最后一個到達屏障的線程會被阻塞
3、通過 reset 重置計數,會使得進入 await 的線程出現BrokenBarrierException;我們可以通過捕獲異常重新處理業務邏輯
4、如果采用是 CyclicBarrier(int parties, Runnable barrierAction) 構造方法,執行 barrierAction 操作的是最后一個到達的線程。

CountDownLatch和CyclicBarrier區別

  • CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置。所以CyclicBarrier能處理更為復雜的業務場景。例如,如果計算發生錯誤,可以重置計數 器,并讓線程重新執行一次。

Semaphore

Semaphore也就是我們常說的信號燈,Semaphore可以控制同時訪問的線程個數,通過 acquire 獲取一個許可,如果沒有就等待,通過 release 釋放一個許可。有點類似限流
的作用。叫信號燈的原因也和他的用處有關,比如某商場就 5 個停車位,每個停車位只能停一輛車,如果這個時候來了 10 輛車,必須要等前面有空的車位才能進入。

Semaphore使用示例

package com.zwx.concurrent.jucUtil;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore=new Semaphore(2);
        for(int i=1;i<=5;i++){
            new Car(i,semaphore).start();
        }
    }
}

class Car extends Thread{
    private int num;
    private Semaphore semaphore;

    public Car(int num, Semaphore semaphore) {
        this.num = num;
        this.semaphore = semaphore;
    }
    @Override
    public void run() {
        try {
            semaphore.acquire();//獲取一個許可
            System.out.println("第"+num+"輛車進來了");

            TimeUnit.SECONDS.sleep(2);
            System.out.println("第"+num+"輛車出去了");
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出結果:


在這里插入圖片描述

我們可以看到,一開始進來2輛車之后就阻塞了,后面必須出去一輛車才能進來一輛車。

Semaphore源碼分析

Semaphore(permits)

Semaphore實際上和CountDownLatch實現非常相似,構造器最終的結果也是調用Sync類中將AbstractQueuedSynchronizer類中的state屬性設置為permits:


在這里插入圖片描述

上圖說明默認構造的是非公平鎖,但是還提供了另一個構造器由我們自己決定使用非公平鎖還是公平鎖,構造器最終是調用的下面這個方法設置state屬性:


在這里插入圖片描述

Semaphore#acquire()

這個方法和CountDownLatch中的await()調用的是同一個方法,這里就不再做分析

Semaphore#release()

這個方法調用的和CountDownLatch中的countDown()調用的也是同一個方法:


在這里插入圖片描述

最終調用的tryReleaseShared(arg)會和CountDownLatch會有一點點差異:

Semaphore#tryReleaseShared()

在這里插入圖片描述

當釋放了一個令牌之后,通過將允許的令牌總數+1實現多進來一個線程。

AQS#doReleaseShared()

上面tryReleaseShared()返回true之后,就會去喚醒下一個線程,這個和上面CountDownLatch中的countDown()方法調用的也是同一個方法去喚醒下一個線程。

總結

本篇文章主要介紹了三個常用的工具CountDownLatch,CyclicBarrier,Semaphore,其中,CountDownLatch和CyclicBarrier在一定場景下是可以替換使用的,而Semaphore一般用于限流。

后面并發編程系列將繼續介紹JUC包下面隊列如ConcurrentLinkedQueue和阻塞隊列等相關知識。感興趣的 請關注我,一起學習進步

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。