Java 多線程 : JUC 併發工具原理

Java 多線程 : JUC 併發工具原理

首先分享之前的所有文章 , 歡迎點贊收藏轉發三連下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 備份 : 👉 gitee.com/antblack/ca…

一 . 前言

趁着有空 , 趕緊把之前欠的債還上 . 這是多線程一階段計劃的最後一篇 , 後續多線程會轉入修訂和深入階段 . 徹底喫透多線程.

二. 工具介紹

之前說 AQS 的時候曾經提到過這幾個類 , 這幾個類有一些各自的特點 , 很符合特定的場景 , 之前在生產上用的還挺舒服.

我們一般使用的併發工具有四種 :

CyclicBarrier : 放學一起走

  • 允許一組線程互相等待,直到到達某個公共屏障點 (common barrier point)
  • 讓一組線程到達一個屏障時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續幹活

CountDownLatch : 等人到齊了就觸發

  • 在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待
  • 用給定的計數 初始化 CountDownLatch。
  • 由於調用了 countDown() 方法,所以在當前計數到達零之前,await 方法會一直受阻塞。
  • 之後,會釋放所有等待的線程,await 的所有後續調用都將立即返回。這種現象只出現一次——計數無法被重置。
  • CountDownLatch是通過一個計數器來實現的,當我們在new 一個CountDownLatch對象的時候需要帶入該計數器值,該值就表示了線程的數量。
  • 每當一個線程完成自己的任務後,計數器的值就會減1。當計數器的值變爲0時,就表示所有的線程均已經完成了任務

Semaphore

  • 信號量Semaphore是一個控制訪問多個共享資源的計數器,和CountDownLatch一樣,其本質上是一個“共享鎖”。

Exchanger

  • 可以在對中對元素進行配對和交換的線程的同步點
  • 每個線程將條目上的某個方法呈現給 exchange 方法,與夥伴線程進行匹配,並且在返回時接收其夥伴的對象 , Exchanger 可能被視爲 SynchronousQueue 的雙向形式

三 .原理解析

3 .1 CyclicBarrier

作用 :

它允許一組線程互相等待,直到到達某個公共屏障點 (Common Barrier Point)。在涉及一組固定大小的線程的程序中,這些線程必須不時地互相等待,此時 CyclicBarrier 很有用。因爲該 Barrier 在釋放等待線程後可以重用,所以稱它爲循環( Cyclic ) 的 屏障( Barrier ) 。

內部原理 :

內部使用重入鎖ReentrantLock 和 Condition

構造函數 :

  • CyclicBarrier(int parties):

    • 創建一個新的 CyclicBarrier,它將在給定數量的參與者(線程)處於等待狀態時啓動,
    • 但它不會在啓動 barrier 時執行預定義的操作。
  • CyclicBarrier(int parties, Runnable barrierAction) :

    • 創建一個新的 CyclicBarrier,它將在給定數量的參與者(線程)處於等待狀態時啓動,
    • 並在啓動 barrier 時執行給定的屏障操作,該操作由最後一個進入 barrier 的線程執行。

使用變量 :

  • parties 變量 : 表示攔截線程的總數量。
  • count 變量 : 表示攔截線程的剩餘需要數量。
  • barrierAction 變量 : 爲 CyclicBarrier 接收的 Runnable 命令,用於在線程到達屏障時,優先執行 barrierAction ,用於處理更加複雜的業務場景。
  • generation 變量 : 表示 CyclicBarrier 的更新換代
// 常用方法 : 
M- await : 等待狀態
M- await(long timeout, TimeUnit unit) : 等待超時
M- dowait
    - 該方法第一步會試着獲取鎖 
    - 如果分代已經損壞,拋出異常
    - 如果線程中斷,終止CyclicBarrier
    - 進來線程 ,--count 
    - count == 0 表示所有線程均已到位,觸發Runnable任務
    - 喚醒所有等待線程,並更新generation
    > 跳出等待狀態的方法
        - 最後一個線程到達,即index == 0
        - 超出了指定時間(超時等待)
        - 其他的某個線程中斷當前線程
        - 其他的某個線程中斷另一個等待的線程
        - 其他的某個線程在等待barrier超時
        - 其他的某個線程在此barrier調用reset()方法。reset()方法用於將屏障重置爲初始狀態。
SC- Generation : 描述了 CyclicBarrier 的更新換代。
    - 在CyclicBarrier中,同一批線程屬於同一代。
    - 當有 parties 個線程全部到達 barrier 時,generation 就會被更新換代。
    - 其中 broken 屬性,標識該當前 CyclicBarrier 是否已經處於中斷狀態
M- breakBarrier : 終止所有的線程
M- nextGeneration : 更新換代操作
	- 1. 喚醒所有線程。
	- 2. 重置 count 。
	- 3. 重置 generation 。
M- reset : 重置 barrier 到初始化狀態 
M- getNumberWaiting : 獲得等待的線程數
M- 判斷 CyclicBarrier 是否處於中斷
    
複製代碼

CyclicBarrier.jpg

使用案例 :

Gitee CyclicBarrier 使用

問題補充 :


// 問題一 : 攔截的核心
1. 傳入總得 Count 數
2. 每次進來都會 --count , 同時判斷 count ==0 
3. 如果不爲 0 ,當前線程就會阻塞

// 問題二 : 涉及源碼
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();

複製代碼

3.2 CountDownLatch

在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待

用給定的計數 初始化 CountDownLatch。由於調用了 countDown() 方法,所以在當前計數到達零之前,await 方法會一直受阻塞。之後,會釋放所有等待的線程,await 的所有後續調用都將立即返回。這種現象只出現一次, 計數無法被重置。如果需要重置計數,請考慮使用 CyclicBarrier

CountDownLatch是通過一個計數器來實現的,當我們在new 一個CountDownLatch對象的時候需要帶入該計數器值,該值就表示了線程的數量。每當一個線程完成自己的任務後,計數器的值就會減1。當計數器的值變爲0時,就表示所有的線程均已經完成了任務

// 內部主要方法
> CountDownLatch內部依賴Sync實現,而Sync繼承AQS

> sync :
    : tryAcquireShared 獲取同步狀態
    : tryReleaseShared 釋放同步狀態
	
> await() :
    使當前線程在鎖存器倒計數至零之前一直等待,除非線程被中斷
    : sync.acquireSharedInterruptibly(1);
    : 內部使用AQS的acquireSharedInterruptibly(int arg)
	
> getState()
    : 獲取同步狀態,其值等於計數器的值
    : 從這裏我們可以看到如果計數器值不等於0,則會調用doAcquireSharedInterruptibly(int arg)

> doAcquireSharedInterruptibly
    : 自旋方法會嘗試一直去獲取同步狀態
	
> countDown
    : CountDownLatch提供countDown() 方法遞減鎖存器的計數,如果計數到達零,則釋放所有等待的線程
    : 內部調用AQS的releaseShared(int arg)方法來釋放共享鎖同步狀態
    : tryReleaseShared(int arg)方法被CountDownLatch的內部類Sync重寫    

複製代碼

CountDownLatch.jpg

參考案例

Gitee CountDownLatch 使用

總結

CountDownLatch 內部通過共享鎖實現。在創建CountDownLatch實例時,需要傳遞一個int型的參數:count,該參數爲計數器的初始值,也可以理解爲該共享鎖可以獲取的總次數。

當某個線程調用await()方法,程序首先判斷count的值是否爲0,如果不會0的話則會一直等待直到爲0爲止 (PS : 可以多個線程都調用 await)

當其他線程調用countDown()方法時,則執行釋放共享鎖狀態,使count值 – 1 (PS :countDown 並不會阻塞)

當在創建CountDownLatch時初始化的count參數,必須要有count線程調用countDown方法纔會使計數器count等於0,鎖纔會釋放,前面等待的線程纔會繼續運行。注意CountDownLatch不能回滾重置

3 .3 Semaphore

基礎點

信號量Semaphore是一個控制訪問多個共享資源的計數器,和CountDownLatch一樣,其本質上是一個“共享鎖”

從概念上講,信號量維護了一個許可集。如有必要,在許可可用前會阻塞每一個 acquire(),然後再獲取該許可。每個 release() 添加一個許可,從而可能釋放一個正在阻塞的獲取者。

Semaphore 通常用於限制可以訪問某些資源(物理或邏輯的)的線程數目

當一個線程想要訪問某個共享資源時,它必須要先獲取Semaphore,當Semaphore >0時,獲取該資源並使Semaphore – 1。如果Semaphore值 = 0,則表示全部的共享資源已經被其他線程全部佔用,線程必須要等待其他線程釋放資源。當線程釋放資源時,Semaphore則+1

實現細節

Semaphore提供了兩個構造函數:

  • Semaphore(int permits) :創建具有給定的許可數和非公平的公平設置的 Semaphore。
  • Semaphore(int permits, boolean fair) :創建具有給定的許可數和給定的公平設置的 Semaphore。

Semaphore默認選擇非公平鎖。

當信號量Semaphore = 1 時,它可以當作互斥鎖使用。其中0、1就相當於它的狀態,當=1時表示其他線程可以獲取,當=0時,排他,即其他線程必須要等待。

//------ 信號量獲取
> acquire()方法來獲取一個許可
	: 內部調用AQS的acquireSharedInterruptibly(int arg),該方法以共享模式獲取同步狀態
	

> 公平 
    : 判斷該線程是否位於CLH隊列的列頭
    : 獲取當前的信號量許可
    : 設置“獲得acquires個信號量許可之後,剩餘的信號量許可數”
    : CAS設置信號量
> 非公平
    : 不需要判斷當前線程是否位於CLH同步隊列列頭
     
複製代碼

Semaphore.jpg

3 .4 Exchanger

可以在對中對元素進行配對和交換的線程的同步點

每個線程將條目上的某個方法呈現給 exchange 方法,與夥伴線程進行匹配,並且在返回時接收其夥伴的對象 , Exchanger 可能被視爲 SynchronousQueue 的雙向形式

Exchanger,它允許在併發任務之間交換數據 : 當兩個線程都到達同步點時,他們交換數據結構,因此第一個線程的數據結構進入到第二個線程中,第二個線程的數據結構進入到第一個線程中

TODO : Exchanger 的源代碼比較繞 ,而且這個組件使用場景並不多 , 所以先留個坑 , 以後項目上真的有場景了再實際上分析一下

3.5 併發工具使用

@ github.com/black-ant/c…

補充 :

# CountDownLatch 和 CyclicBarrier 如何理解 ?

  • CyclicBarrier : 小學生去郊遊 , 老師下車時統計人數 ,人數到齊了才能一起參觀
  • CountDownLatch : 幼兒園老師送孩子(ChildThread)放學 , 走一個記一個數 ,當所有的學生放學後 , 老師(BossThread)下班
// 核心解釋 : 
CyclicBarrier 就是一堵牆 , 人數到了所有線程才能一起越過牆
CountDownLatch 只是一個計數器 , 數目到了主線程才能執行

// 其他要點 : 
CyclicBarrier 可以重置計數 , CountDownLatch 不可以
複製代碼

總結

終於補上了最後一塊板 , 後面來真正的深入多線程看看吧 , 爭取早日成爲多線程大師段位

更新記錄

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