前言
JUC中提供了很多同步工具類,比如CountDownLatch、CyclicBarrier、Semaphore等,都可以作用同步手段來實現多線程之間的同步效果
一、CountDownLatch
1.1、CountDownLatch的使用
CountDownLatch可以理解為是同步計數器,作用是允許一個或多個線程等待其他線程執行完成之後才繼續執行,比如打dota、LoL或者王者榮耀時,創建了一個五人房,只有當五個玩家都準備了之後,遊戲才能正式開始,否則遊戲主線程會一直等待著直到玩家全部準備。在玩家沒準備之前,遊戲主線程會一直處於等待狀態。如果把CountDownLatch比做此場景都話,相當於開始定義了匹配遊戲需要5個線程,只有當5個線程都準備完成了之後,主線程才會開始進行匹配操作。
CountDownLatch案例如下:
執行結果如下:
等待玩家準備中……遊戲房間等待玩家加入……線程Thread-2組隊準備,還需等待4人準備線程Thread-3組隊準備,還需等待3人準備線程Thread-4組隊準備,還需等待2人準備線程Thread-5組隊準備,還需等待1人準備線程Thread-6組隊準備,還需等待0人準備遊戲匹配中……遊戲房間已鎖定……線程Thread-7組隊準備,房間已滿不可加入線程Thread-8組隊準備,房間已滿不可加入線程Thread-9組隊準備,房間已滿不可加入線程Thread-10組隊準備,房間已滿不可加入線程Thread-11組隊準備,房間已滿不可加入線程Thread-12組隊準備,房間已滿不可加入線程Thread-13組隊準備,房間已滿不可加入
本案例中有兩個線程都調用了latch.await()方法,則這兩個線程都會被阻塞,直到條件達成。當5個線程調用countDown方法之後,達到了計數器的要求,則後續再執行countDown方法的效果就無效了,因為CountDownLatch僅一次有效。
1.2、CountDownLatch的實現原理
CountDownLatch的實現原理主要是通過內部類Sync來實現的,內部類Sync是AQS的子類,主要是通過重寫AQS的共享式獲取和釋放同步狀態方法來實現的。源碼如下:
CountDownLatch初始化時需要定義調用count的次數,然後每調用一次countDown方法都會計數減一,源碼如下:
可以看出CountDownLatch的實現邏輯全部都是調用內部類Sync的對應方法實現的,Sync源碼如下:
通過內部類Sync的源碼可以分析出,CountDownLatch的實現完整邏輯如下:
1、初始化CountDownLatch實際就是設置了AQS的state為計數的值
2、調用CountDownLatch的countDown方法時實際就是調用AQS的釋放同步狀態的方法,每調用一次就自減一次state值
3、調用await方法實際就調用AQS的共享式獲取同步狀態的方法acquireSharedInterruptibly(1),這個方法的實現邏輯就調用子類Sync的tryAcquireShared方法,只有當子類Sync的tryAcquireShared方法返回大於0的值時才算獲取同步狀態成功,
否則就會一直在死循環中不斷重試,直到tryAcquireShared方法返回大於等於0的值,而Sync的tryAcquireShared方法只有當AQS中的state值為0時才會返回1,否則都返回-1,也就相當於只有當AQS的state值為0時,await方法才會執行成功,否則
就會一直處於死循環中不斷重試。
總結:
CountDownLatch實際完全依靠AQS的共享式獲取和釋放同步狀態來實現,初始化時定義AQS的state值,每調用countDown實際就是釋放一次AQS的共享式同步狀態,await方法實際就是嘗試獲取AQS的同步狀態,只有當同步狀態值為0時才能獲取成功。
二、CyclicBarrier
2.1、CyclicBarrier的使用
CyclicBarrier可以理解為一個循環同步屏障,定義一個同步屏障之後,當一組線程都全部達到同步屏障之前都會被阻塞,直到最後一個線程達到了同步屏障之後才會被打開,其他線程才可繼續執行。
還是以dota、LoL和王者榮耀為例,當第一個玩家準備了之後,還需要等待其他4個玩家都準備,遊戲才可繼續,否則準備的玩家會被一直處於等待狀態,只有當最後一個玩家準備了之後,遊戲才會繼續執行。
CyclicBarrier使用案例如下:
執行結果如下1:
線程Thread-0組隊準備,當前1人已準備線程Thread-1組隊準備,當前2人已準備線程Thread-2組隊準備,當前3人已準備線程Thread-3組隊準備,當前4人已準備線程Thread-4組隊準備,當前5人已準備線程:Thread-4開始組隊遊戲線程:Thread-0開始組隊遊戲線程:Thread-1開始組隊遊戲線程:Thread-2開始組隊遊戲線程:Thread-3開始組隊遊戲線程Thread-5組隊準備,當前1人已準備線程Thread-6組隊準備,當前2人已準備線程Thread-7組隊準備,當前3人已準備線程Thread-8組隊準備,當前4人已準備線程Thread-9組隊準備,當前5人已準備線程:Thread-9開始組隊遊戲線程:Thread-5開始組隊遊戲線程:Thread-7開始組隊遊戲線程:Thread-6開始組隊遊戲線程:Thread-8開始組隊遊戲線程Thread-10組隊準備,當前1人已準備線程Thread-11組隊準備,當前2人已準備
本案例中定義了達到同步屏障的線程為5個,每當一個線程調用了barrier.await()方法之後表示該線程已達到屏障,此時當前線程會被阻塞,只有當最後一個線程調用了await方法之後,被阻塞的其他線程才會被喚醒繼續執行。
另外CyclicBarrier是循環同步屏障,同步屏障打開之後立馬會繼續計數,等待下一組線程達到同步屏障。而CountDownLatch僅單次有效。
2.2、CyclicBarrier的實現原理
先看下CyclicBarrier的構造方法
CyclicBarrier的構造方法沒有特殊之處,主要是給兩個屬性parties(總線程數)、count(當前剩餘線程數)進行賦值,這裡需要兩個值的原因是CyclicBarrier提供了重置的功能,當調用reset方法重置時就需要將count值再賦值成parties的值
再看下await方法的實現邏輯
從源碼可以看出CyclicBarrier的實現原理主要是通過ReentrantLock和Condition來實現的,主要實現流程如下:
1、創建CyclicBarrier時定義了CyclicBarrier對象需要達到的線程數count
2、每當一個線程執行了await方法時,需要先通過ReentrantLock進行加鎖操作,然後對count進行自減操作,操作成功則判斷當前count是否為0;
3、如果當前count不為0則調用Condition的await方法使當前線程進入等待狀態;
4、如果當前count為0則表示同步屏障已經完全,此時調用Condition的signalAll方法喚醒之前所有等待的線程,並開啟循環的下一次同步屏障功能;
5、喚醒其他線程之後,其他線程繼續執行剩餘的邏輯。
2.3、通過Synchronized和wait/notify實現CyclicBarrier
通過分析了解了CyclicBarrier是通過ReentrantLock和Condition來實現的,而ReentrantLock+Condition在使用上基本上等同於Synchronized+wait/notify,既然如此就可以通過Synchronized+wait/notify來自定義一個CyclicBarrier,話不多說,代碼如下:
執行結果如下2:
線程Thread-0組隊準備,當前1人已準備 線程Thread-1組隊準備,當前2人已準備 線程Thread-2組隊準備,當前3人已準備 線程Thread-3組隊準備,當前4人已準備 線程Thread-4組隊準備,當前5人已準備 線程:Thread-4開始組隊遊戲 線程:Thread-3開始組隊遊戲 線程:Thread-0開始組隊遊戲 線程:Thread-1開始組隊遊戲 線程:Thread-2開始組隊遊戲 線程Thread-5組隊準備,當前1人已準備 線程Thread-6組隊準備,當前2人已準備 線程Thread-7組隊準備,當前3人已準備 線程Thread-8組隊準備,當前4人已準備 線程Thread-9組隊準備,當前5人已準備 線程:Thread-9開始組隊遊戲 線程:Thread-7開始組隊遊戲 線程:Thread-5開始組隊遊戲 線程:Thread-6開始組隊遊戲 線程:Thread-8開始組隊遊戲 線程Thread-10組隊準備,當前1人已準備 線程Thread-11組隊準備,當前2人已準備
可以看出實現的效果和CyclicBarrier實現的效果完全一樣
三、Semaphore
3.1、Semaphore的使用
Semaphore字面意思是信號量,實際可以看作是一個限流器,初始化Semaphore時就定義好了最大通行證數量,每次調用時調用方法來消耗,業務執行完畢則釋放通行證,如果通行證消耗完,再獲取通行證時就需要阻塞線程直到有通行證可以獲取。
比如銀行櫃檯的窗口,一共有5個窗口可以使用,當窗口都被佔用時,後面來的人就需要排隊等候,直到有窗口用戶辦理完業務離開之後後面的人才可繼續爭取。模擬代碼如下:
執行結果如下:
初始化5個銀行櫃檯窗口用戶Thread-0佔用窗口用戶Thread-0開始辦理業務用戶Thread-1佔用窗口用戶Thread-1開始辦理業務用戶Thread-2佔用窗口用戶Thread-2開始辦理業務用戶Thread-3佔用窗口用戶Thread-3開始辦理業務用戶Thread-4佔用窗口用戶Thread-4開始辦理業務用戶Thread-0離開窗口用戶Thread-5佔用窗口用戶Thread-5開始辦理業務用戶Thread-1離開窗口用戶Thread-6佔用窗口用戶Thread-6開始辦理業務用戶Thread-2離開窗口用戶Thread-7佔用窗口用戶Thread-7開始辦理業務用戶Thread-3離開窗口用戶Thread-8佔用窗口用戶Thread-8開始辦理業務用戶Thread-4離開窗口用戶Thread-9佔用窗口用戶Thread-9開始辦理業務用戶Thread-5離開窗口用戶Thread-6離開窗口用戶Thread-7離開窗口用戶Thread-8離開窗口用戶Thread-9離開窗口
可以看出前5個線程可以直接佔用窗口,但是後5個線程需要等待前面的線程離開了窗口之後才可佔用窗口。
Semaphore調用acquire方法獲取許可證,可以同時獲取多個,但是也需要對應的釋放多個,否則會造成其他線程獲取不到許可證的情況。一旦許可證被消耗完,那麼線程就需要被阻塞,直到許可證被釋放才可繼續執行。
另外Semaphore還具有公平模式和非公平模式兩種用法,公平模式則遵循FIFO原則先排隊的線程先拿到許可證;非公平模式則自行爭取。
3.2、Semaphore實現原理
Semaphore的構造方法
構造方法只有兩個參數,一個是許可證總數量,一個是是否為公平模式;默認是非公平模式
Semaphore的實現全部是通過其內部類Sync來實現了,Sync也是AQS的子類,Semaphore的實現方式基本上和ReentrantLock的實現原理如出一轍。
公平模式實現原理:
公平模式就是噹噹前線程是AQS同步隊列首節點的後繼節點時才有權利嘗試獲取共享式的同步狀態,並將同步狀態值減去需要佔用的許可證數量,如果剩餘許可證數量小於0則表示獲取失敗進入AQS的死循環不停重試;
如果許可證數量大於0並且CAS設置成功了,則返回剩餘許可證數量表示搶佔許可證成功;
非公平模式實現原理:
看我公平模式的實現基本是就可以猜到非公平模式是如何實現的,只是會少了一步判斷當前節點是否是首節點的後繼節點而已。
了解完Semaphore的公平模式和非公平模式的佔有許可證的方法,再分析釋放許可證的方法,不過可以先自行猜測下會是如何實現的,既然獲取許可證是通過state欄位不斷減少來實現的,那麼毫無疑問釋放許可證就肯定是不斷給state增加來實現的。
釋放許可證源碼如下:
Semaphore的釋放許可證實際就是調用AQS的共享式釋放同步狀態的方法,然後調用內部類Sync重寫的AQS的tryReleaseShared方法,實現邏輯就是不停CAS設置state的值加上需要釋放的數量,直到CAS成功。這裡少了AQS的邏輯解析,有興趣可自行回顧AQS的共享式釋放同步狀態的實現原理。
四、Extra Knowledge
4.1、CountDownLatch 和 CyclicBarrier的區別?
CountDownLatch和CyclicBarrier實現的效果看似都是某個線程等待一組線程達到條件之後才可繼續執行,但是實際上兩者存在很多區別。
1、CountDownLatch阻塞的是調用await()的線程,不會阻塞達到條件的線程;CyclicBarrier阻塞的是達到同步屏障的所有線程
2、CountDownLatch採用倒數計數,定義數量之後,每當一個線程達到要求之後就減一;CyclicBarrier是正數計數,當數量達到定義的數量之後就打開同步屏障
3、CountDownLatch僅單次有效,不可重複使用;CyclicBarrir可以循環重複使用
4、CountDownLatch定義的數量和實際線程數無關,可以有一個線程執行多次countDown();CyclicBarrier定義的數量和實際線程數一致,必須由多個線程都達到要求執行才行(線程調用await()方法之後就會被阻塞,想調用多次也不行的)
5、CountDownLatch是通過內部類Sync繼承AQS來實現的;CyclicBarrier是通過重入鎖ReentrantLock來實現的
6、CountDownLatch不可重置;CyclicBarrier可以調用reset方法進行重置