三個好用的併發工具類

YangAM發表於2019-03-02

以前的文章中,我們介紹了太多的底層原理技術以及新概念,本篇我們輕鬆點,瞭解下 Java 併發包下、基於這些底層原理的三個框架工具類。

它們分別是:

  • 訊號量 Semaphore
  • 倒數計時門栓 CountDownLatch
  • 屏障 CyclicBarrier

所以,既然是工具類,那麼必然是離不開特定的場景的,於是相互之間沒有誰優誰劣,只有誰更合適。

訊號量 Semaphore

Semaphore 適用於什麼樣的使用場景呢,我們舉個通俗的例子:

假如現在有一個停車場,裡面有隻十個停車位,當著十個停車位都被佔用了,外面的車就不允許進入了,就必須在外面等著。出來一輛車才允許進去一輛車

這個場景不同於我們一般的併發場景,一般來說,我們的臨界資源只能允許一個執行緒進行訪問,其他執行緒都地等著。

但是,有一種場景是,臨界資源允許多個執行緒同時訪問,超過限定數量的外的執行緒得阻塞等待。

這種情境使用原始的那一套也是能實現的,但那叫「造輪子」,Java 併發框架下給我們提供了一個工具類,專門適用這種場景。

Semaphore 可以說是為上述這種場景而生的一個工具類,我們寫個 demo 實現上述邏輯:

image

執行程式之後,你會看到:

image

你看,出來一個執行緒才允許進去一個執行緒,這就是 Semaphore。

semaphore 的內部原理其實你去看原始碼,你會發現和我們的 ReentrantLock 的實現是極其類似的,包括公平與非公平策略的支援,只不過,AQS 裡面的 state 在前者的實現中,一般小於等於一(除非重入鎖),而後者的 state 則小於等於十,記錄的是剩餘可用臨界資源數量。

所以,semaphore 天生就存在一個問題,如果某個執行緒重入了臨界區,可用臨界資源的數量是否需要減少?

停車場一共十個停車位,一輛車進去並佔有了一個停車位,過了一段時間,這個向管理員報告,我還要佔用一個停車位,先不管他佔兩個幹啥,此時的管理員會同意嗎?

實際上,在 Java 這個管理員看來,已經進入臨界區的執行緒是「老爺」,提出的要求都會優先滿足,即便他自身佔有的資源並沒有釋放。

所以,在 Semaphore 機制裡,一個執行緒進入臨界區之後佔用掉所有的臨界資源都是可能的。

倒數計時門栓 CountDownLatch

下面我們來看看這個 CountDownLatch,名字聽起來挺高階,究竟提供了怎樣的功能呢?

有這麼一個常見的場景,我們一起來看看:

大家日常經常使用的拼多多,一件商品至少需要兩到三人拼團,商家才會發貨。

這裡,我們不去研究它的商業模式,不管他是怎麼實現盈利的,就這麼一種場景,如果要用基本的併發 API 來實現,你可能會想到:

來一個執行緒阻塞一次,知道達到指定的數量後,全部喚醒

對,沒錯,CountDownLatch 內部就是這樣實現的,輪子已經幫你造好了,我們來看看該怎麼實現上述的模型案例:

image

多執行幾次,你會發現結果不會錯,拼團的人先後順序可能不同,但商家一定是在三個人都準備好了之後才會發貨。

除此之外,它還有更多的應用,比如百米賽跑,只有當所有運動員都準備好了之後,裁判員才會吹響哨子,等等等等。

實現原理也基本和顯式鎖類似,不同點依然在於對 state 的控制,CountDownLatch 只判斷 state 是否等於零,不等於零就說明時機未到,阻塞當前執行緒。

而每一次的 countDown 方法呼叫都會減少一次倒數計時資源,直至為零才喚醒阻塞的執行緒。

迴圈屏障 CyclicBarrier

CyclicBarrier 其實和 CountDownLatch 很像,我們先介紹完 CyclicBarrier,然後再和你一起去比較比較他倆的區別和相似點。

考慮這麼一個場景:

公寓的班車總是在公寓樓下裝滿一車人之後,出發並開到地鐵站,接著再回來接下一班人。

這麼一個場景,我們考慮該怎麼實現:

image

效果大概就是這個樣子:

image

CyclicBarrier 就像一個屏障,例項化的時候需要傳入兩個引數,第一個引數指定我們的屏障最多攔截多少個執行緒後就開啟屏障,第二個引數指明最後一個到達屏障的執行緒需要額外做的操作。

一般而言,最後一個執行緒到達屏障後,屏障將會開啟,釋放前面所有的執行緒,並在最後重新關上屏障。

CyclicBarrier 只需要用到一個 await 就可以完成所有的功能,我們總結下該方法的實現邏輯:

  1. 首先,減少一次可用資源數量
  2. 如果可用資源數為零,則說明自己是最後一個執行緒,於是會執行我們傳入的額外操作,喚醒所有已經到達在等待的執行緒,並重新開啟一個屏障計數。
  3. 否則說明自己不是最後一個執行緒,於是將自身執行緒在一個迴圈當中阻塞到一個條件佇列上

好了,看完 CyclicBarrier 你會發現,它真的很類似我們的倒數計時門栓,下面我們就來闡述他倆的區別與聯絡。

第一個區別

倒數計時門栓 CountDownLatch 一旦被開啟後就不能再次合上,也是說只要被呼叫了足夠次數的 countDown,await 方法就會失效,它是一次性的。

CyclicBarrier 是迴圈發生的,當最後一個執行緒到達屏障,會優先重置屏障計數,屏障再次開啟攔截阻隔。

第二個區別

CountDownLatch 是計數器, 執行緒來一個就記一個,此期間不阻塞執行緒,當達到指定數量之後才會去喚醒外部等待的執行緒,也就是說外部是有一個乃至多個執行緒等待一個條件滿足之後才能繼續執行,而這個條件就是滿足一定數量的執行緒,這樣才能啟用當前外部執行緒的繼續執行。

CyclicBarrier 像一個柵欄,來一個執行緒阻塞一個,直到阻塞了指定數量的執行緒後,一次性全部啟用,讓他們同時執行,像一個百米衝刺一樣。

最後的最後

好了,以上就是我們 Java 併發包下面比較好用的三個工具類,其中前兩個的底層實現幾乎完全依賴顯式鎖的原理方法,後一個則是使用的顯式鎖加條件變數重新造的輪子,都是非常好用的工具!

除此之外還要說一點的是,整個併發這塊內容,基本核心的東西我們都已經介紹完了,共計十四篇文章,從基本的執行緒概念,到鎖原理,到執行緒池,再到非同步任務,自認為總結的足夠細緻了,不知道你瞭解了多少呢?

記不住沒關係,我也為你提供了一份思維導圖的總結,羅列了上述基本的內容,你可以對照著進行回顧,同時也歡迎你私信我討論探究。

獲取方式:公眾號回覆「併發」或是直接去我的 Github 下載。

ps:我也要放假啦,祝福大家新春快樂,春節期間就不更文了,節後我們將開啟新的篇章,系統的總結『資料庫』相關的技術及原理,盡請關注!

關注公眾不迷路,一個愛分享的程式設計師。

公眾號回覆「1024」加作者微信一起探討學習!

每篇文章用到的所有案例程式碼素材都會上傳我個人 github

github.com/SingleYam/o…

歡迎來踩!
YangAM 公眾號

相關文章