《吃透Java》- 并發(fā)何須懼,工具來(lái)相助!
大家好,我是小菜。一個(gè)希望能夠成為 吹著牛X談架構(gòu) 的男人!如果你也想成為我想成為的人,不然點(diǎn)個(gè)關(guān)注做個(gè)伴,讓小菜不再孤單!

本文主要介紹
搬磚必備的并發(fā)工具類來(lái)都來(lái)了,點(diǎn)個(gè)在看怎么了~!
微信公眾號(hào)已開(kāi)啟,小菜良記,沒(méi)關(guān)注的同學(xué)們記得關(guān)注哦!
作為一名躺平的搬磚工程師,在內(nèi)卷時(shí)期,慢條斯理地搬磚可能已經(jīng)離你而去。磚是一種共享資源,現(xiàn)如今每個(gè)搬磚工都想追求質(zhì)量的又要同時(shí)保持高效的搬磚速率,在爭(zhēng)奪的情況下會(huì)不會(huì)出現(xiàn)并發(fā)的情況?你搬過(guò)的磚卻計(jì)算在別人的KPI上,原本只想 躺平,卻沒(méi)想到躺平也要遭受如此不公!原本只需煎一面的咸魚(yú),現(xiàn)在還得把另一面翻過(guò)來(lái)再煎~!
終于,躺平的搬磚工決定不再躺平,他捏緊了拳頭,牙齒咬得“格格”作響,他的臉像蠟一樣的黃,嘴唇咬得發(fā)白,原本不多的頭發(fā)一顫一顫地,全身都在瑟瑟地發(fā)抖,狠狠的下定了決定:我一定要解決并發(fā)問(wèn)題!讓搬磚行業(yè)正常的運(yùn)行~!
什么?正常運(yùn)行,那就得解決并發(fā)問(wèn)題!
好了好了,氣氛對(duì)頭了,這個(gè)時(shí)候小菜緩步登場(chǎng),那么就進(jìn)入主題,解決并發(fā)問(wèn)題你常用的并發(fā)工具類有哪些?
在 JDK 的并發(fā)包中已經(jīng)提供了幾個(gè)非常有用的并發(fā)工具類。
CountDownLatch CyclicBarrier Semaphore Exchanger
這幾個(gè)可能有些小伙伴看的眼熟,可能有點(diǎn)生分,看的眼熟卻不會(huì)用和看的生分的也并無(wú)區(qū)別。那接下來(lái)我們通過(guò)簡(jiǎn)單的闡述,就能讓你在平時(shí)的開(kāi)發(fā)中運(yùn)用自如!
一、 CountDownLatch
這是個(gè)在平時(shí)開(kāi)發(fā)中出現(xiàn)頻率較高的并發(fā)工具,它是一個(gè) 倒計(jì)數(shù)器。是一個(gè)非常實(shí)用的多線程控制工具類,這個(gè)工具類常常用來(lái)控制線程等待,可以讓一個(gè)線程等待直到計(jì)數(shù)器結(jié)束再開(kāi)始執(zhí)行!
我們不必一開(kāi)始就深究源碼,先會(huì)用再善用。因此我們簡(jiǎn)單看個(gè)簡(jiǎn)易的例子
《一個(gè)都不能少》
小王老師是一個(gè)嚴(yán)格的老師,她上課有些許任性,必須等到所有學(xué)生(10名)都到場(chǎng)后才會(huì)開(kāi)始上課,也就是但凡一個(gè)學(xué)生不在場(chǎng),都不會(huì)開(kāi)課。
我們要遵循 一個(gè)都不能少 的要求,也就是當(dāng)學(xué)生人數(shù) < 總?cè)藬?shù)的時(shí)候不能執(zhí)行上課的這個(gè)動(dòng)作。那么這個(gè)時(shí)候我們應(yīng)該怎么處理這個(gè)問(wèn)題呢?
我們課前點(diǎn)名,增加一個(gè) if 判斷,當(dāng)人數(shù)不滿足的情況下,就不會(huì)進(jìn)入到 上課 的動(dòng)作中。這個(gè)可能是一個(gè)慣性思維,大部分同學(xué)都會(huì)這樣操作。那么問(wèn)題來(lái)了,有些學(xué)生可能只是因?yàn)檫t到,錯(cuò)過(guò)了點(diǎn)名的判斷,當(dāng) if 執(zhí)行結(jié)束后就不會(huì)再判斷,那么錯(cuò)過(guò)就是錯(cuò)過(guò),盡管后續(xù)人數(shù)已經(jīng)到齊了,但最終是開(kāi)不了課的!
想想再改進(jìn)下,如果因?yàn)?if 只判斷一次而造成的問(wèn)題,那我們能不能一直判斷,那就可以用到了while 或者 for 一直循環(huán)判斷。解決思路是正確的,那我們就順藤引出 CountDownLatch 的用法

代碼不長(zhǎng),但不知道結(jié)果是否如我們所愿:

我們可以看到,當(dāng)10名學(xué)生都達(dá)到后,小王老師開(kāi)始上課了,但如果我們這是一個(gè)學(xué)生沒(méi)到達(dá)呢?

當(dāng)達(dá)到人數(shù)未符合預(yù)期,則不能正常上課,目前看已經(jīng)滿足我們的需求了。那我們緊接著模擬一下學(xué)生遲到的場(chǎng)景~

依然是學(xué)號(hào)為 10 的同學(xué),雖遲但到,課還是可以正常進(jìn)行上的!
看來(lái) CountDownLatch 真是一個(gè)好工具,簡(jiǎn)簡(jiǎn)單單就幫我們解決了該問(wèn)題!那他怎么解決的呢?

CountDownLatch 是通過(guò)一個(gè)計(jì)數(shù)器來(lái)實(shí)現(xiàn)的,首先設(shè)置一個(gè)計(jì)數(shù)器的初始值。每當(dāng)完成一個(gè)任務(wù)后,計(jì)數(shù)器的值就會(huì)減1,當(dāng)計(jì)數(shù)器達(dá)到0 時(shí),它表示所有任務(wù)都已經(jīng)完成,然后在閉鎖上等待的線程可以恢復(fù)執(zhí)行任務(wù).
我們首先可以要看的是 CountDownLatch 的構(gòu)造方法
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
該方法需要初始化一個(gè)計(jì)數(shù)值,并初始化一個(gè) Sync, 我們這個(gè)時(shí)候不妨大膽猜測(cè),CountDownLatch底層便是靠 Sync 實(shí)現(xiàn)的!我們來(lái)看看 Sync 是個(gè)啥玩意?

可以看到在Sync內(nèi)部維護(hù)著一個(gè)安全變量 state,它的值便是 計(jì)數(shù)器的值。其中有兩個(gè)重要方法:tryAcquireShared(int acquires) 和 tryReleaseShared(int releases)。那這兩個(gè)方法有什么用呢?
我們可以先回到 CountDownLatch 類中,上面我們已經(jīng)看到該類構(gòu)造函數(shù)的作用,接下來(lái)需要認(rèn)識(shí)其中兩個(gè)重要的方法:countDown() 和 await()。在我們看來(lái),countDown() 方法便是用來(lái)將計(jì)數(shù)值減 1, await() 方法是用來(lái)阻塞判斷計(jì)數(shù)值是否為 0?那我們進(jìn)入對(duì)應(yīng)方法看是如何實(shí)現(xiàn)的
public void countDown() {
sync.releaseShared(1);
}
---
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
這兩個(gè)方法調(diào)用的都是 AQS 中的兩個(gè)方法:(我這邊直接貼源碼注釋,仔細(xì)看哦~!)
countDown()

await()

上面便是 CountDownLatch 的實(shí)現(xiàn),那我們不妨想想該工具類在實(shí)時(shí)系統(tǒng)中的使用場(chǎng)景:
實(shí)現(xiàn)最大的并行性
當(dāng)我們想要同時(shí)啟動(dòng)多個(gè)線程,實(shí)現(xiàn)最大程度的并行性。例如,我們想測(cè)試一個(gè)單例類,如果我們創(chuàng)建一個(gè)初始值為 1 的CountDownLatch,并讓所有線程都在這個(gè)鎖上等待,那么我們就可以很輕松的完成測(cè)試,只需要調(diào)用一次 **countDown()**方法就可以讓所有等待線程同時(shí)恢復(fù)執(zhí)行
開(kāi)始執(zhí)行前等待 n 個(gè)線程完成各自的任務(wù)
當(dāng)我們應(yīng)用程序執(zhí)行前,確保某些前置動(dòng)作需要執(zhí)行
死鎖檢測(cè)
我們可以使用 n 個(gè)線程訪問(wèn)共享資源,在每次測(cè)試階段的線程數(shù)目是不同的,這樣可以嘗試產(chǎn)生死鎖
二、CyclicBarrier
CyclicBarrier是另外一種多線程并發(fā)控制工具。Cyclic 意為循環(huán),也就是說(shuō)這個(gè)計(jì)數(shù)器可以反復(fù)使用,它比CountDownLatch更加強(qiáng)大一點(diǎn),它要做的事情是,讓一組線程到達(dá)一個(gè)屏障(也可以叫同步點(diǎn))時(shí)被阻塞,直到最后一個(gè)線程達(dá)到屏障時(shí),屏障才會(huì)開(kāi)門(mén),所有被屏障攔截的線程才會(huì)繼續(xù)工作。
也就是說(shuō) CyclicBarrier 是加法計(jì)時(shí)器,我們一樣通過(guò)以上 《一個(gè)都不能少》 例子來(lái)示例如何使用

這里就不再演示缺課與遲到的示范,與上述 CountDownLatch 實(shí)現(xiàn)方式一致
這里我們依然關(guān)注兩個(gè)方法,一個(gè)是構(gòu)造方法,一個(gè)是 await()
我們依然進(jìn)入到 CyclicBarrier 類中查看構(gòu)造方法
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
可以發(fā)現(xiàn)和上面說(shuō)到的 CountDownLatch 還是有出入的,該構(gòu)造方法只是做了屏障點(diǎn)的記錄,我們重點(diǎn)還是要看 await() 方法
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
追根朔底我們得看 dowait() 方法,進(jìn)入方法可以發(fā)現(xiàn)實(shí)現(xiàn)方式并不復(fù)雜。由于代碼有點(diǎn)長(zhǎng),我們截取重點(diǎn)說(shuō)明
與 CountDownLatch 不同的是,屏障點(diǎn)變量并沒(méi)有使用 volatile 修飾,那么毋庸就得加鎖使之線程安全!



以上便是 CyclicBarrier 的整個(gè)實(shí)現(xiàn)過(guò)程,具體咱就不摳細(xì)節(jié)了~!
CyclicBarrier 和 CountDownLatch 還是有點(diǎn)類似的,但是我們要清楚他們之間的區(qū)別:
CountDownLatch: 一個(gè)線程(或多個(gè)),等待另外 N 個(gè)線程完成某件事情之后才會(huì)執(zhí)行 CyclicBarrier: N 個(gè)線程之間相互等待,任何一個(gè)線程完成之前,所有的線程都必須等待
比較重要的一點(diǎn):CountDownLatch 不可重復(fù)利用,CyclicBarrier 不可重復(fù)利用
三、Semaphore
信號(hào)量(Semaphore)是為多線程提供了更為強(qiáng)大的控制方法。從廣義上來(lái)講,信號(hào)量是對(duì)鎖的擴(kuò)展。無(wú)論是內(nèi)部鎖synchronized還是重入鎖ReentrantLock,一次都只允許一個(gè)線程訪問(wèn)一個(gè)資源,而信號(hào)量卻可以指定多個(gè)線程,同時(shí)訪問(wèn)某一個(gè)共享資源。
我們簡(jiǎn)單看個(gè)簡(jiǎn)易的例子
《搶車位》
原本一個(gè)小區(qū)有 5 個(gè)地上停車位已經(jīng)可以很好的滿足業(yè)主的停車需求,但是這兩年車輛數(shù)暴增,幾乎家家一車,車位自然供不應(yīng)求,那只能遵循先到先得的原則!

然后我們看下執(zhí)行結(jié)果:

可以看到 5 個(gè)車位是共享資源,只有先到的業(yè)主才能搶到車位,當(dāng)搶到車位的業(yè)主離開(kāi)后,后續(xù)的業(yè)主才能進(jìn)入獲取到車位!
我們提取出關(guān)注點(diǎn)構(gòu)造方法、acquire()、release()
構(gòu)造方法
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
是的,Semaphore 有兩個(gè)構(gòu)造方法,區(qū)別在于是否使用公平鎖。然后我們繼續(xù)看 aquire()、release()
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void release() {
sync.releaseShared(1);
}
excuse me~? 前面有認(rèn)真看的小伙伴,肯定覺(jué)得眼熟了,這調(diào)用的方法豈不是和上面 CountDownLatch 的一樣?是的,這兩個(gè)并發(fā)工具類,底層都是 調(diào)用 AQS 的線程方法。如果不知道這兩個(gè)方法作用的同學(xué),可以上翻查看,這里不再贅述!
根據(jù)這個(gè)工具類結(jié)合上述例子,我們可以在流量控制的時(shí)候使用!特別是公共資源有限的應(yīng)用場(chǎng)景,比如數(shù)據(jù)庫(kù)連接,假如有一個(gè)需求要讀取幾萬(wàn)個(gè)文件的數(shù)據(jù),因?yàn)槎际?IO 密集型的任務(wù),我們可以啟動(dòng)幾十個(gè)線程去并發(fā)地讀取,但是我們得經(jīng)過(guò)硬盤(pán)->內(nèi)存->數(shù)據(jù)庫(kù),而如果數(shù)據(jù)庫(kù)的連接數(shù)只有10個(gè),那我們這個(gè)時(shí)候就必須要控制只有 10 個(gè)線程可以同時(shí)獲取數(shù)據(jù)庫(kù)連接保存數(shù)據(jù),這個(gè)時(shí)候就可以使用 Semaphore 來(lái)做流量控制~!
四、Exchanger
看到這個(gè)名稱,不知道有多少小伙伴腦子里想的是 這是啥?。實(shí)話說(shuō),這個(gè)工具類出鏡率真不高,用的比較少。Exchanger 是一個(gè)用于線程間協(xié)作的工具類。它可用于線程間的數(shù)據(jù)交換,它提供了一個(gè)同步點(diǎn),兩個(gè)線程可以交換彼此的數(shù)據(jù),。這兩個(gè)線程通過(guò) Exchanger 方法交換數(shù)據(jù),如果第一個(gè)線程先執(zhí)行 exchange() 方法, 它會(huì)一直等待第二個(gè)線程也執(zhí)行 exchanger() 方法,當(dāng)兩個(gè)線程都到達(dá)同步點(diǎn)時(shí),這兩個(gè)線程就可以交換數(shù)據(jù),將本線程生產(chǎn)出來(lái)的數(shù)據(jù)傳遞給對(duì)方。
這里注意的是 兩個(gè)線程,不存在**"三角關(guān)系"**

在沒(méi)有經(jīng)過(guò) exchange() 時(shí),數(shù)字線程 打印的應(yīng)該是數(shù)字,字母線程打印的應(yīng)該是字母,但是經(jīng)過(guò)了 exchange()結(jié)果就發(fā)生了逆轉(zhuǎn):

注意: 如果兩個(gè)線程中有一個(gè)沒(méi)有執(zhí)行 exchange() 方法,那么則會(huì)一直等待

為了避免這種情況的發(fā)生,我們可以在 exchange()中加上超時(shí)時(shí)間!
那么這個(gè)工具類有什么應(yīng)用場(chǎng)景呢?我們想想如果在一個(gè)線程的執(zhí)行任務(wù)中創(chuàng)建某個(gè)對(duì)象的生產(chǎn)代價(jià)很高,而另外一個(gè)線程任務(wù)也需要消費(fèi)到這個(gè)對(duì)象,那我們就可以借助 Exchanger 來(lái)幫助我們傳輸類對(duì)象。甚至于可以實(shí)現(xiàn) 生產(chǎn)者-消費(fèi)者模式!
以上便是幾種并發(fā)工具類的使用與應(yīng)用場(chǎng)景,當(dāng)然上面提到的應(yīng)用場(chǎng)景只是一小部分,更多的當(dāng)然需要在開(kāi)發(fā)中繼續(xù)挖掘,做到會(huì)用且善用
看到最后,搬磚工程師掐滅了手中的煙頭,煙霧彌漫的空氣中傳來(lái)一句經(jīng)久不滅的話語(yǔ):他娘的,沒(méi)想到這年頭搬個(gè)磚都不容易了
不要空談,不要貪懶,和小菜一起做個(gè)吹著牛X做架構(gòu)的程序猿吧~點(diǎn)個(gè)關(guān)注做個(gè)伴,讓小菜不再孤單。咱們下文見(jiàn)!

今天的你多努力一點(diǎn),明天的你就能少說(shuō)一句求人的話!
我是小菜,一個(gè)和你一起變強(qiáng)的男人。
??微信公眾號(hào)已開(kāi)啟,小菜良記,沒(méi)關(guān)注的同學(xué)們記得關(guān)注哦!
