四十七、線程協(xié)作

本篇將介紹控制并發(fā)流程的工具類奸攻,作用就是更容易地讓線程之間相互配合蒜危,比如讓線程 A 等待線程 B 執(zhí)行完畢后再繼續(xù)執(zhí)行,來(lái)滿足業(yè)務(wù)邏輯睹耐。

1辐赞、Semaphore 信號(hào)量

1.1 基本介紹

從圖中可以看出,信號(hào)量的一個(gè)最主要的作用就是硝训,來(lái)控制那些需要限制并發(fā)訪問(wèn)量的資源响委。具體來(lái)講,信號(hào)量會(huì)維護(hù)“許可證”的計(jì)數(shù)窖梁,而線程去訪問(wèn)共享資源前赘风,必須先拿到許可證。線程可以從信號(hào)量中去“獲取”一個(gè)許可證纵刘,一旦線程獲取之后邀窃,信號(hào)量持有的許可證就轉(zhuǎn)移過(guò)去了,所以信號(hào)量手中剩余的許可證要減一假哎。

同理瞬捕,線程也可以“釋放”一個(gè)許可證鞍历,如果線程釋放了許可證,這個(gè)許可證相當(dāng)于被歸還給信號(hào)量了肪虎,于是信號(hào)量中的許可證的可用數(shù)量加一劣砍。當(dāng)信號(hào)量擁有的許可證數(shù)量減到 0 時(shí),如果下個(gè)線程還想要獲得許可證扇救,那么這個(gè)線程就必須等待秆剪,直到之前得到許可證的線程釋放,它才能獲取爵政。由于線程在沒有獲取到許可證之前不能進(jìn)一步去訪問(wèn)被保護(hù)的共享資源仅讽,所以這就控制了資源的并發(fā)訪問(wèn)量,這就是整體思路钾挟。

1.2 使用場(chǎng)景

在這個(gè)場(chǎng)景中洁灵,我們的服務(wù)是中間這個(gè)方塊兒,左側(cè)是請(qǐng)求掺出,右側(cè)是我們所依賴的那個(gè)慢服務(wù)徽千。出于種種原因(比如計(jì)算量大、依賴的下游服務(wù)多等)汤锨,右邊的慢服務(wù)速度很慢双抽,并且它可以承受的請(qǐng)求數(shù)量也很有限,一旦有太多的請(qǐng)求同時(shí)到達(dá)它這邊闲礼,可能會(huì)導(dǎo)致它這個(gè)服務(wù)不可用牍汹,會(huì)壓垮它。所以我們必須要保護(hù)它柬泽,不能讓太多的線程同時(shí)去訪問(wèn)慎菲。那怎么才能做到這件事情呢?

那就是利用信號(hào)量控制許可證的發(fā)放和歸還锨并,進(jìn)而去控制在同一時(shí)刻最多只有 3 個(gè)線程執(zhí)行訪問(wèn)慢服務(wù)器的任務(wù)露该。

1.3 使用流程

來(lái)看一下具體的用法,使用流程主要分為以下三步:

  • 首先初始化一個(gè)信號(hào)量第煮,并且傳入許可證的數(shù)量解幼,這是它的帶公平參數(shù)的構(gòu)造函數(shù):public Semaphore(int permits, boolean fair),傳入兩個(gè)參數(shù)包警,第一個(gè)參數(shù)是許可證的數(shù)量撵摆,另一個(gè)參數(shù)是是否公平。如果第二個(gè)參數(shù)傳入 true揽趾,則代表它是公平的策略台汇,會(huì)把之前已經(jīng)等待的線程放入到隊(duì)列中,而當(dāng)有新的許可證到來(lái)時(shí),它會(huì)把這個(gè)許可證按照順序發(fā)放給之前正在等待的線程苟呐;如果這個(gè)構(gòu)造函數(shù)第二個(gè)參數(shù)傳入 false痒芝,則代表非公平策略,也就有可能插隊(duì)牵素,就是說(shuō)后進(jìn)行請(qǐng)求的線程有可能先得到許可證严衬。

  • 第二個(gè)流程是在建立完這個(gè)構(gòu)造函數(shù),初始化信號(hào)量之后笆呆,就可以利用 acquire() 方法请琳。在調(diào)用慢服務(wù)之前,讓線程來(lái)調(diào)用 acquire 方法或者 acquireUninterruptibly方法赠幕,這兩個(gè)方法的作用是要獲取許可證俄精,這同時(shí)意味著只有這個(gè)方法能順利執(zhí)行下去的話,它才能進(jìn)一步訪問(wèn)這個(gè)代碼后面的調(diào)用慢服務(wù)的方法榕堰。如果此時(shí)信號(hào)量已經(jīng)沒有剩余的許可證了竖慧,那么線程就會(huì)等在 acquire 方法的這一行代碼中,所以它也不會(huì)進(jìn)一步執(zhí)行下面調(diào)用慢服務(wù)的方法逆屡。正是用這種方法圾旨,保護(hù)了慢服務(wù)。
    acquire() 和 acquireUninterruptibly() 的區(qū)別是:是否能響應(yīng)中斷魏蔗。acquire() 是可以支持中斷的砍的,也就是說(shuō),它在獲取信號(hào)量的期間莺治,假設(shè)這個(gè)線程被中斷了廓鞠,那么它就會(huì)跳出 acquire() 方法,不再繼續(xù)嘗試獲取了产雹。而 acquireUninterruptibly() 方法是不會(huì)被中斷的诫惭。

  • 第三步就是在任務(wù)執(zhí)行完畢之后,調(diào)用 release() 來(lái)釋放許可證蔓挖,比如說(shuō)在執(zhí)行完慢服務(wù)這行代碼之后,再去執(zhí)行 release() 方法馆衔,這樣一來(lái)瘟判,許可證就會(huì)還給信號(hào)量了。

除了這幾個(gè)主要方法以外角溃,還有一些其他的方法:
(1)public boolean tryAcquire()
tryAcquire 和之前介紹鎖的 trylock 思維是一致的拷获,是嘗試獲取許可證,相當(dāng)于看看現(xiàn)在有沒有空閑的許可證减细,如果有就獲取匆瓜,如果現(xiàn)在獲取不到也沒關(guān)系,不必陷入阻塞,可以去做別的事驮吱。

(2)public boolean tryAcquire(long timeout, TimeUnit unit)
同樣有一個(gè)重載的方法茧妒,它里面?zhèn)魅肓顺瑫r(shí)時(shí)間。比如傳入了 3 秒鐘左冬,則意味著最多等待 3 秒鐘桐筏,如果等待期間獲取到了許可證,則往下繼續(xù)執(zhí)行拇砰;如果超時(shí)時(shí)間到梅忌,依然獲取不到許可證,它就認(rèn)為獲取失敗除破,且返回 false牧氮。

(3)availablePermits()
這個(gè)方法用來(lái)查詢可用許可證的數(shù)量,返回一個(gè)整型的結(jié)果瑰枫。

代碼示例:

public class SemaphoreDemo2 {

    static Semaphore semaphore = new Semaphore(3);

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(50);
        for (int i = 0; i < 1000; i++) {
            service.submit(new Task());
        }
        service.shutdown();
    }

    static class Task implements Runnable {

        @Override
        public void run() {
            try {
                semaphore.acquire();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "拿到了許可證踱葛,花費(fèi)2秒執(zhí)行慢服務(wù)");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("慢服務(wù)執(zhí)行完畢," + Thread.currentThread().getName() + "釋放了許可證");
            semaphore.release();
        }
    }
}

特殊用法:一次性獲取或釋放多個(gè)許可證
比如 semaphore.acquire(2)躁垛,里面?zhèn)魅雲(yún)?shù) 2剖毯,這就叫一次性獲取兩個(gè)許可證。同時(shí)釋放也是一樣的教馆,semaphore.release(3) 相當(dāng)于一次性釋放三個(gè)許可證逊谋。

為什么要這樣做呢?列舉一個(gè)使用場(chǎng)景土铺。比如說(shuō)第一個(gè)任務(wù) A(Task A )會(huì)調(diào)用很耗資源的方法一 method1()胶滋,而任務(wù) B 調(diào)用的是方法二 method 2,但這個(gè)方法不是特別消耗資源悲敷。在這種情況下究恤,假設(shè)一共有 5 個(gè)許可證,只能允許同時(shí)有 1 個(gè)線程調(diào)用方法一后德,或者同時(shí)最多有 5 個(gè)線程調(diào)用方法二部宿,但是方法一和方法二不能同時(shí)被調(diào)用。

所以瓢湃,就要求 Task A 在執(zhí)行之前要一次性獲取到 5 個(gè)許可證才能執(zhí)行理张,而 Task B 只需要獲取一個(gè)許可證就可以執(zhí)行了。這樣就避免了任務(wù) A 和 B 同時(shí)運(yùn)行绵患,同時(shí)又很好的兼顧了效率雾叭,不至于同時(shí)只允許一個(gè)線程訪問(wèn)方法二,那樣的話也存在浪費(fèi)資源的情況落蝙,所以可以根據(jù)自己的需求合理地利用信號(hào)量的許可證來(lái)分配資源织狐。

注意點(diǎn):
(1)獲取和釋放的許可證數(shù)量盡量保持一致暂幼,否則比如每次都獲取 2 個(gè)但只釋放 1 個(gè)甚至不釋放,那么信號(hào)量中的許可證就慢慢被消耗完了移迫,最后導(dǎo)致里面沒有許可證了旺嬉,那其他的線程就再也沒辦法訪問(wèn)了;
(2)在初始化的時(shí)候可以設(shè)置公平性起意,如果設(shè)置為 true 則會(huì)讓它更公平鹰服,但如果設(shè)置為 false 則會(huì)讓總的吞吐量更高。
(3)信號(hào)量是支持跨線程揽咕、跨線程池的悲酷,而且并不是哪個(gè)線程獲得的許可證,就必須由這個(gè)線程去釋放亲善。事實(shí)上设易,對(duì)于獲取和釋放許可證的線程是沒有要求的,比如線程 A 獲取了然后由線程 B 釋放蛹头,這完全是可以的顿肺,只要邏輯合理即可。

1.4 信號(hào)量能被 FixedThreadPool 替代嗎渣蜗?

這個(gè)問(wèn)題相當(dāng)于屠尊,信號(hào)量是可以限制同時(shí)訪問(wèn)的線程數(shù),那為什么不直接用固定數(shù)量線程池去限制呢耕拷?這樣不是更方便嗎讼昆?比如說(shuō)線程池里面有 3 個(gè)線程,那自然最多只有 3 個(gè)線程去訪問(wèn)了骚烧。

在實(shí)際業(yè)務(wù)中會(huì)遇到這樣的情況:假如浸赫,在調(diào)用慢服務(wù)之前需要有個(gè)判斷條件,比如只想在每天的零點(diǎn)附近去訪問(wèn)這個(gè)慢服務(wù)時(shí)受到最大線程數(shù)的限制(比如 3 個(gè)線程)赃绊,而在除了每天零點(diǎn)附近的其他大部分時(shí)間既峡,是希望讓更多的線程去訪問(wèn)的。所以在這種情況下就應(yīng)該把線程池的線程數(shù)量設(shè)置為 50 碧查,甚至更多运敢,然后在執(zhí)行之前加一個(gè) if 判斷,如果符合時(shí)間限制了(比如零點(diǎn)附近)忠售,再用信號(hào)量去額外限制者冤,這樣做是比較合理的。

再說(shuō)一個(gè)例子档痪,比如說(shuō)在大型應(yīng)用程序中會(huì)有不同類型的任務(wù),它們也是通過(guò)不同的線程池來(lái)調(diào)用慢服務(wù)的邢滑。因?yàn)檎{(diào)用方不只是一處腐螟,可能是 Tomcat 服務(wù)器或者網(wǎng)關(guān)愿汰,我們就不應(yīng)該限制,或者說(shuō)也無(wú)法做到限制它們的線程池的大小乐纸。但可以做的是衬廷,在執(zhí)行任務(wù)之前用信號(hào)量去限制一下同時(shí)訪問(wèn)的數(shù)量,因?yàn)樾盘?hào)量具有跨線程汽绢、跨線程池的特性吗跋,所以即便這些請(qǐng)求來(lái)自于不同的線程池,我們也可以限制它們的訪問(wèn)宁昭。如果用 FixedThreadPool 去限制跌宛,那就做不到跨線程池限制了,這樣的話會(huì)讓功能大大削弱积仗。

基于以上的理由疆拘,如果想要限制并發(fā)訪問(wèn)的線程數(shù),用信號(hào)量是更合適的寂曹。

2哎迄、CountDownLatch 是如何安排線程執(zhí)行順序的?

2.1 基本介紹

CountDownLatch 是 JDK 提供的并發(fā)流程控制的工具類隆圆,它是在 java.util.concurrent 包下漱挚,在 JDK1.5 以后加入的。
CountDownLatch 類在創(chuàng)建實(shí)例的時(shí)候渺氧,需要在構(gòu)造函數(shù)中傳入倒數(shù)次數(shù)旨涝,然后由需要等待的線程去調(diào)用 await 方法開始等待,而每一次其他線程調(diào)用了 countDown 方法之后阶女,計(jì)數(shù)便會(huì)減 1颊糜,直到減為 0 時(shí),之前等待的線程便會(huì)繼續(xù)運(yùn)行秃踩。

下面舉個(gè)例子來(lái)說(shuō)明它主要在什么場(chǎng)景下使用衬鱼。
比如我們?nèi)ビ螛?lè)園坐激流勇進(jìn),有的時(shí)候游樂(lè)園里人不是那么多憔杨,這時(shí)鸟赫,管理員會(huì)讓你稍等一下,等人坐滿了再開船消别,這樣的話可以在一定程度上節(jié)約游樂(lè)園的成本抛蚤。座位有多少,就需要等多少人寻狂,這就是 CountDownLatch 的核心思想岁经,等到一個(gè)設(shè)定的數(shù)值達(dá)到之后,才能出發(fā)蛇券。

把激流勇進(jìn)的例子用流程圖的方式來(lái)表示:

可以看到缀壤,最開始 CountDownLatch 設(shè)置的初始值為 3樊拓,然后 T0 線程上來(lái)就調(diào)用 await 方法,它的作用是讓這個(gè)線程開始等待塘慕,等待后面的 T1筋夏、T2、T3图呢,它們每一次調(diào)用 countDown 方法条篷,3 這個(gè)數(shù)值就會(huì)減 1,也就是從 3 減到 2蛤织,從 2 減到 1赴叹,從 1 減到 0,一旦減到 0 之后瞳筏,這個(gè) T0 就相當(dāng)于達(dá)到了自己觸發(fā)繼續(xù)運(yùn)行的條件稚瘾,于是它就恢復(fù)運(yùn)行了。

2.2 主要方法介紹

(1)構(gòu)造函數(shù):public CountDownLatch(int count) { };
它的構(gòu)造函數(shù)是傳入一個(gè)參數(shù)姚炕,該參數(shù) count 是需要倒數(shù)的數(shù)值摊欠。

(2)await():調(diào)用 await() 方法的線程開始等待,直到倒數(shù)結(jié)束柱宦,也就是 count 值為 0 的時(shí)候才會(huì)繼續(xù)執(zhí)行些椒。

(3)await(long timeout, TimeUnit unit):await() 有一個(gè)重載的方法,里面會(huì)傳入超時(shí)參數(shù)掸刊,這個(gè)方法的作用和 await() 類似免糕,但是這里可以設(shè)置超時(shí)時(shí)間,如果超時(shí)就不再等待了忧侧。

(4)countDown():把數(shù)值倒數(shù) 1石窑,也就是將 count 值減 1,直到減為 0 時(shí)蚓炬,之前等待的線程會(huì)被喚起松逊。

2.3 CountDownLatch 的兩個(gè)典型用法

用法一:一個(gè)線程等待其他多個(gè)線程都執(zhí)行完畢,再繼續(xù)自己的工作

舉個(gè)生活中的例子肯夏,那就是運(yùn)動(dòng)員跑步的場(chǎng)景经宏,比如在比賽跑步時(shí)有 5 個(gè)運(yùn)動(dòng)員參賽,終點(diǎn)有一個(gè)裁判員驯击,什么時(shí)候比賽結(jié)束呢烁兰?那就是當(dāng)所有人都跑到終點(diǎn)之后,這相當(dāng)于裁判員等待 5 個(gè)運(yùn)動(dòng)員都跑到終點(diǎn)徊都,宣布比賽結(jié)束沪斟。我們用代碼的形式來(lái)寫出運(yùn)動(dòng)員跑步的場(chǎng)景,代碼如下:

public class RunDemo1 {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i + 1;
            Runnable runnable = new Runnable() {

                @Override
                public void run() {
                    try {
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println(no + "號(hào)運(yùn)動(dòng)員完成了比賽暇矫。");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        latch.countDown();
                    }
                }
            };
            service.submit(runnable);
        }
        System.out.println("等待5個(gè)運(yùn)動(dòng)員都跑完.....");
        latch.await();
        System.out.println("所有人都跑完了币喧,比賽結(jié)束轨域。");
    }
}

用法二:多個(gè)線程等待某一個(gè)線程的信號(hào),同時(shí)開始執(zhí)行

這和第一個(gè)用法有點(diǎn)相反杀餐,再列舉一個(gè)實(shí)際的場(chǎng)景,比如在運(yùn)動(dòng)會(huì)上朱巨,剛才說(shuō)的是裁判員等運(yùn)動(dòng)員史翘,現(xiàn)在是運(yùn)動(dòng)員等裁判員。在運(yùn)動(dòng)員起跑之前都會(huì)等待裁判員發(fā)號(hào)施令冀续,一聲令下運(yùn)動(dòng)員統(tǒng)一起跑琼讽,用代碼把這件事情描述出來(lái),如下所示:

public class RunDemo2 {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("運(yùn)動(dòng)員有5秒的準(zhǔn)備時(shí)間");
        CountDownLatch countDownLatch = new CountDownLatch(1);
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i + 1;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println(no + "號(hào)運(yùn)動(dòng)員準(zhǔn)備完畢洪唐,等待裁判員的發(fā)令槍");
                    try {
                        countDownLatch.await();
                        System.out.println(no + "號(hào)運(yùn)動(dòng)員開始跑步了");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            service.submit(runnable);
        }
        Thread.sleep(5000);
        System.out.println("5秒準(zhǔn)備時(shí)間已過(guò)钻蹬,發(fā)令槍響,比賽開始凭需!");
        countDownLatch.countDown();
    }
}

注意點(diǎn):
(1)剛才講了兩種用法问欠,其實(shí)這兩種用法并不是孤立的,甚至可以把這兩種用法結(jié)合起來(lái)粒蜈,比如利用兩個(gè) CountDownLatch顺献,第一個(gè)初始值為多個(gè),第二個(gè)初始值為 1枯怖,這樣就可以應(yīng)對(duì)更復(fù)雜的業(yè)務(wù)場(chǎng)景注整;
(2)CountDownLatch 是不能夠重用的,比如已經(jīng)完成了倒數(shù)度硝,那可不可以在下一次繼續(xù)去重新倒數(shù)呢肿轨?這是做不到的,如果有這個(gè)需求蕊程,可以考慮使用 CyclicBarrier 或者創(chuàng)建一個(gè)新的 CountDownLatch 實(shí)例椒袍。

3、CyclicBarrier 和 CountdownLatch 有什么異同存捺?

3.1 CyclicBarrier 的作用

CyclicBarrier 和 CountDownLatch 確實(shí)有一定的相似性槐沼,它們都能阻塞一個(gè)或者一組線程,直到某種預(yù)定的條件達(dá)到之后捌治,這些之前在等待的線程才會(huì)統(tǒng)一出發(fā)岗钩,繼續(xù)向下執(zhí)行。但它們的作用并不是完全一樣的肖油。

CyclicBarrier 可以構(gòu)造出一個(gè)集結(jié)點(diǎn)兼吓,當(dāng)某一個(gè)線程執(zhí)行 await() 的時(shí)候,它就會(huì)到這個(gè)集結(jié)點(diǎn)開始等待森枪,等待這個(gè)柵欄被撤銷视搏。直到預(yù)定數(shù)量的線程都到了這個(gè)集結(jié)點(diǎn)之后审孽,這個(gè)柵欄就會(huì)被撤銷,之前等待的線程就在此刻統(tǒng)一出發(fā)浑娜,繼續(xù)去執(zhí)行剩下的任務(wù)佑力。

舉一個(gè)生活中的例子。假設(shè)班級(jí)春游去公園里玩筋遭,并且會(huì)租借三人自行車打颤,每個(gè)人都可以騎,但由于這輛自行車是三人的漓滔,所以要湊齊三個(gè)人才能騎一輛编饺,而且從公園大門走到自行車驛站需要一段時(shí)間。那么模擬這個(gè)場(chǎng)景响驴,寫出如下代碼:

public class CyclicBarrierDemo {

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        for (int i = 0; i < 6; i++) {
            new Thread(new Task(i + 1, cyclicBarrier)).start();
        }
    }

    static class Task implements Runnable {

        private int id;
        private CyclicBarrier cyclicBarrier;

        public Task(int id, CyclicBarrier cyclicBarrier) {
            this.id = id;
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            System.out.println("同學(xué)" + id + "現(xiàn)在從大門出發(fā)透且,前往自行車驛站");
            try {
                Thread.sleep((long) (Math.random() * 10000));
                System.out.println("同學(xué)" + id + "到了自行車驛站,開始等待其他人到達(dá)");
                cyclicBarrier.await();
                System.out.println("同學(xué)" + id + "開始騎車");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

3.2 執(zhí)行動(dòng)作 barrierAction

public CyclicBarrier(int parties, Runnable barrierAction):當(dāng) parties 線程到達(dá)集結(jié)點(diǎn)時(shí)豁鲤,繼續(xù)往下執(zhí)行前秽誊,會(huì)執(zhí)行這一次這個(gè)動(dòng)作。

接下來(lái)再介紹一下它的一個(gè)額外功能畅形,就是執(zhí)行動(dòng)作 barrierAction 功能养距。CyclicBarrier 還有一個(gè)構(gòu)造函數(shù)是傳入兩個(gè)參數(shù)的,第一個(gè)參數(shù)依然是 parties日熬,代表需要幾個(gè)線程到齊棍厌;第二個(gè)參數(shù)是一個(gè) Runnable 對(duì)象,它就是下面所要介紹的 barrierAction竖席。

當(dāng)預(yù)設(shè)數(shù)量的線程到達(dá)了集結(jié)點(diǎn)之后耘纱,在出發(fā)的時(shí)候,便會(huì)執(zhí)行這里所傳入的 Runnable 對(duì)象毕荐,那么假設(shè)把剛才那個(gè)代碼的構(gòu)造函數(shù)改成如下這個(gè)樣子:

CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
    @Override
    public void run() {
        System.out.println("湊齊3人了束析,出發(fā)!");
    }
});

可以看出憎亚,傳入了第二個(gè)參數(shù)员寇,它是一個(gè) Runnable 對(duì)象,在這里傳入了這個(gè) Runnable 之后第美,這個(gè)任務(wù)就會(huì)在到齊的時(shí)候先去打印"湊齊3人了蝶锋,出發(fā)!"什往,然后在繼續(xù)執(zhí)行cyclicBarrier.await();之后的代碼System.out.println("同學(xué)" + id + "開始騎車");扳缕。

值得注意的是,這個(gè)語(yǔ)句每個(gè)周期只打印一次,不是說(shuō)有幾個(gè)線程在等待就打印幾次躯舔,而是說(shuō)這個(gè)任務(wù)只在“開閘”的時(shí)候執(zhí)行一次驴剔。所以 CyclicBarrier 的 barrierAction 特別適合用于下面的場(chǎng)景:一組子任務(wù)分別去執(zhí)行各自的邏輯,然后把一個(gè)匯總各個(gè)子任務(wù)結(jié)果的任務(wù)放到 barrierAction 中去執(zhí)行粥庄,當(dāng)每一個(gè)子任務(wù)都執(zhí)行完畢之后丧失,匯總各個(gè)子任務(wù)結(jié)果的任務(wù)自動(dòng)啟動(dòng),使它們形成一個(gè)配合飒赃。

3.3 CyclicBarrier 和 CountDownLatch 的異同?

相同點(diǎn):都能阻塞一個(gè)或一組線程利花,直到某個(gè)預(yù)設(shè)的條件達(dá)成發(fā)生,再統(tǒng)一出發(fā)载佳。

不同點(diǎn):
(1)作用對(duì)象不同:CyclicBarrier 要等固定數(shù)量的線程都到達(dá)了柵欄位置才能繼續(xù)執(zhí)行,而 CountDownLatch 只需等待數(shù)字倒數(shù)到 0臀栈,也就是說(shuō) CountDownLatch 作用于事件蔫慧,但 CyclicBarrier 作用于線程;CountDownLatch 是在調(diào)用了 countDown 方法之后把數(shù)字倒數(shù)減 1权薯,而 CyclicBarrier 是在某線程開始等待后把計(jì)數(shù)減 1姑躲。
(2)可重用性不同:CountDownLatch 在倒數(shù)到 0 并且觸發(fā)門閂打開后,就不能再次使用了盟蚣,除非新建一個(gè)新的實(shí)例黍析;而 CyclicBarrier 可以重復(fù)使用,并不需要重新新建實(shí)例屎开。CyclicBarrier 還可以隨時(shí)調(diào)用 reset 方法進(jìn)行重置阐枣,如果重置時(shí)有線程已經(jīng)調(diào)用了 await 方法并開始等待,那么這些線程則會(huì)拋出 BrokenBarrierException 異常奄抽。
(3)執(zhí)行動(dòng)作不同:CyclicBarrier 有執(zhí)行動(dòng)作 barrierAction蔼两,而 CountDownLatch 沒這個(gè)功能。

4逞度、Condition额划、object.wait() 和 notify() 的關(guān)系?

4.1 Condition接口的作用

假設(shè)線程 1 需要等待某些條件滿足后档泽,才能繼續(xù)運(yùn)行俊戳,這個(gè)條件會(huì)根據(jù)業(yè)務(wù)場(chǎng)景不同,有不同的可能性馆匿,比如等待某個(gè)時(shí)間點(diǎn)到達(dá)或者等待某些任務(wù)處理完畢抑胎。在這種情況下,就可以執(zhí)行 Condition 的 await 方法甜熔,一旦執(zhí)行了該方法圆恤,這個(gè)線程就會(huì)進(jìn)入 WAITING 狀態(tài)。

通常會(huì)有另外一個(gè)線程,把它稱作線程 2盆昙,它去達(dá)成對(duì)應(yīng)的條件羽历,直到這個(gè)條件達(dá)成之后,那么淡喜,線程 2 調(diào)用 Condition 的 signal 方法 [或 signalAll 方法]秕磷,代表“這個(gè)條件已經(jīng)達(dá)成了,之前等待這個(gè)條件的線程現(xiàn)在可以蘇醒了”炼团。這個(gè)時(shí)候澎嚣,JVM 就會(huì)找到等待該 Condition 的線程,并予以喚醒瘟芝,根據(jù)調(diào)用的是 signal 方法或 signalAll 方法易桃,會(huì)喚醒 1 個(gè)或所有的線程。于是锌俱,線程 1 在此時(shí)就會(huì)被喚醒晤郑,然后它的線程狀態(tài)又會(huì)回到 Runnable 可執(zhí)行狀態(tài)。

代碼案例:

public class ConditionDemo {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    void method1() throws InterruptedException {
        lock.lock();
        try{
            System.out.println(Thread.currentThread().getName()+":條件不滿足贸宏,開始await");
            condition.await();
            System.out.println(Thread.currentThread().getName()+":條件滿足了造寝,開始執(zhí)行后續(xù)的任務(wù)");
        }finally {
            lock.unlock();
        }
    }

    void method2() throws InterruptedException {
        lock.lock();
        try{
            System.out.println(Thread.currentThread().getName()+":需要5秒鐘的準(zhǔn)備時(shí)間");
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName()+":準(zhǔn)備工作完成,喚醒其他的線程");
            condition.signal();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConditionDemo conditionDemo = new ConditionDemo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    conditionDemo.method2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        conditionDemo.method1();
    }
}

在這個(gè)代碼中吭练,有以下三個(gè)方法诫龙。

  • method1,它代表主線程將要執(zhí)行的內(nèi)容鲫咽,首先獲取到鎖签赃,打印出“條件不滿足,開始 await”浑侥,然后調(diào)用 condition.await() 方法姊舵,直到條件滿足之后,則代表這個(gè)語(yǔ)句可以繼續(xù)向下執(zhí)行了寓落,于是打印出“條件滿足了括丁,開始執(zhí)行后續(xù)的任務(wù)”,最后會(huì)在 finally 中解鎖伶选。

  • method2史飞,它同樣也需要先獲得鎖,然后打印出“需要 5 秒鐘的準(zhǔn)備時(shí)間”仰税,接著用 sleep 來(lái)模擬準(zhǔn)備時(shí)間构资;在時(shí)間到了之后,則打印出“準(zhǔn)備工作完成”陨簇,最后調(diào)用 condition.signal() 方法吐绵,把之前已經(jīng)等待的線程喚醒。

  • main 方法,它的主要作用是執(zhí)行上面這兩個(gè)方法己单,它先去實(shí)例化我們這個(gè)類唉窃,然后再用子線程去調(diào)用這個(gè)類的 method2 方法,接著用主線程去調(diào)用 method1 方法纹笼。

最終這個(gè)代碼程序運(yùn)行結(jié)果如下所示:

main:條件不滿足纹份,開始 await
Thread-0:需要 5 秒鐘的準(zhǔn)備時(shí)間
Thread-0:準(zhǔn)備工作完成,喚醒其他的線程
main:條件滿足了廷痘,開始執(zhí)行后續(xù)的任務(wù)

同時(shí)也可以看到蔓涧,打印這行語(yǔ)句它所運(yùn)行的線程,第一行語(yǔ)句和第四行語(yǔ)句打印的是在 main 線程中笋额,也就是在主線程中去打印的元暴,而第二、第三行是在子線程中打印的兄猩。這個(gè)代碼就模擬了前面所描述的場(chǎng)景错森。

注意點(diǎn):

  • 線程 2 解鎖后妹卿,線程 1 才能獲得鎖并繼續(xù)執(zhí)行

線程 2 對(duì)應(yīng)剛才代碼中的子線程,而線程 1 對(duì)應(yīng)主線程稿茉。這里需要額外注意歼狼,并不是說(shuō)子線程調(diào)用了 signal 之后掏导,主線程就可以立刻被喚醒去執(zhí)行下面的代碼了,而是說(shuō)在調(diào)用了 signal 之后羽峰,還需要等待子線程完全退出這個(gè)鎖趟咆,即執(zhí)行 unlock 之后,這個(gè)主線程才有可能去獲取到這把鎖梅屉,并且當(dāng)獲取鎖成功之后才能繼續(xù)執(zhí)行后面的任務(wù)值纱。剛被喚醒的時(shí)候主線程還沒有拿到鎖,是沒有辦法繼續(xù)往下執(zhí)行的坯汤。

  • signalAll() 和 signal() 區(qū)別

signalAll() 會(huì)喚醒所有正在等待的線程虐唠,而 signal() 只會(huì)喚醒一個(gè)線程。

4.2 Condition 和 wait/notify的關(guān)系

將兩種實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模式的 put 方法進(jìn)行對(duì)比:
(1)Condition 方式

public void put(Object o) throws InterruptedException {
   lock.lock();
   try {
      while (queue.size() == max) {
         condition1.await();
      }
      queue.add(o);
      condition2.signalAll();
   } finally {
      lock.unlock();
   }
}

(2)wait/notify 方式

public synchronized void put() throws InterruptedException {
   while (storage.size() == maxSize) {
      this.wait();
   }
   storage.add(new Object());
   this.notifyAll();
}

(3)對(duì)比兩種方式:

lock.lock() 對(duì)應(yīng)進(jìn)入 synchronized 方法
condition.await() 對(duì)應(yīng) object.wait()
condition.signalAll() 對(duì)應(yīng) object.notifyAll()
lock.unlock() 對(duì)應(yīng)退出 synchronized 方法

如果說(shuō) Lock 是用來(lái)代替 synchronized 的惰聂,那么 Condition 就是用來(lái)代替相對(duì)應(yīng)的 Object 的 wait/notify/notifyAll疆偿,所以在用法和性質(zhì)上幾乎都一樣。

Condition 把 Object 的 wait/notify/notifyAll 轉(zhuǎn)化為了一種相應(yīng)的對(duì)象搓幌,其實(shí)現(xiàn)的效果基本一樣杆故,但是把更復(fù)雜的用法,變成了更直觀可控的對(duì)象方法溉愁,是一種升級(jí)处铛。

await 方法會(huì)自動(dòng)釋放持有的 Lock 鎖,和 Object 的 wait 一樣,不需要自己手動(dòng)釋放鎖撤蟆。

另外奕塑,調(diào)用 await 的時(shí)候必須持有鎖,否則會(huì)拋出異常枫疆,這一點(diǎn)和 Object 的 wait 一樣爵川。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市息楔,隨后出現(xiàn)的幾起案子寝贡,更是在濱河造成了極大的恐慌,老刑警劉巖值依,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件圃泡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡愿险,警方通過(guò)查閱死者的電腦和手機(jī)颇蜡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)辆亏,“玉大人风秤,你說(shuō)我怎么就攤上這事“邕叮” “怎么了缤弦?”我有些...
    開封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)彻磁。 經(jīng)常有香客問(wèn)我碍沐,道長(zhǎng),這世上最難降的妖魔是什么衷蜓? 我笑而不...
    開封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任累提,我火速辦了婚禮,結(jié)果婚禮上磁浇,老公的妹妹穿的比我還像新娘斋陪。我一直安慰自己,他們只是感情好扯夭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開白布鳍贾。 她就那樣靜靜地躺著,像睡著了一般交洗。 火紅的嫁衣襯著肌膚如雪骑科。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天构拳,我揣著相機(jī)與錄音咆爽,去河邊找鬼梁棠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛斗埂,可吹牛的內(nèi)容都是我干的符糊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼呛凶,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼男娄!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起漾稀,我...
    開封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤模闲,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后崭捍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尸折,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年殷蛇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了实夹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡粒梦,死狀恐怖亮航,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情匀们,我是刑警寧澤塞赂,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站昼蛀,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏圆存。R本人自食惡果不足惜叼旋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沦辙。 院中可真熱鬧夫植,春花似錦、人聲如沸油讯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)陌兑。三九已至沈跨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間兔综,已是汗流浹背饿凛。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工狞玛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人涧窒。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓心肪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親纠吴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子硬鞍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355