目錄
一、常見的鎖策略
樂觀鎖VS悲觀鎖
讀寫鎖
重量級鎖VS輕量級鎖
?總結:
自旋鎖(Spin Lock)
公平鎖VS非公平鎖
可重入鎖VS不可重入鎖
二、CAS
何為CAS
CAS有哪些應用
1)實現原子類
2)實現自旋鎖
?CAS的ABA問題
1)什么是ABA問題
2)ABA問題引來的bug
三、synchronized原理
基本特點:
加鎖工作過程
?其他的優化操作
鎖消除
鎖粗化
四、Callable接口
Callable的用法
五、JUC(java.util.concurrent)常見類
ReentrantLock
原子類
線程池
信號量Semaphore
六、CountDownLatch
一、常見的鎖策略
常見的所策略主要有:
- 樂觀鎖,悲觀鎖
- 讀寫鎖
- 重量級鎖,輕量級鎖
- 自旋鎖
- 公平鎖,非公平鎖
- 可重入鎖,不可重入鎖
樂觀鎖VS悲觀鎖
這種是“鎖的一種特性”,即“一類鎖”,不是指具體的一把鎖,而悲觀樂觀是對后續鎖沖突是否激烈(頻繁)給出的預測。
悲觀鎖:
總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖,即預測接下來鎖沖突的概率很大,就應該多做一些工作。
樂觀鎖:
假設數據一般情況下不會產生并發沖突,所以在數據進行提交更新的時候(未加鎖,直接訪問資源),才會正式對數據是否產生并發沖突進行檢測,如果發現并發沖突了,則讓返回用戶的錯誤信息,讓用戶決定如何去做,即如果預測接下來鎖沖突的概率很小,就可以少做一些工作。
synchronized初始使用樂觀鎖策略(未加鎖,直接訪問數據),當發現鎖競爭比較頻繁的時候,就會自動切換成悲觀鎖策略。
樂觀鎖的一個重要功能就是要檢測出數據是否發生訪問沖突,我們可以引入一個“版本號來解決”。
?例如:
假設我們需要多線程修改用戶賬戶余額。設當前余額為100,引入一個版本號version,初始值為1,并且我們規定“提交版本必須大于記錄當前版本才能執行更新余額”。
1)線程1此時從內存中讀出(version=1,balance =100),線程2也讀入此信息(version=1,balance=100)。
2)線程1操作過程中并從其賬戶余額中扣除50(100-50),線程2從其賬戶余額中扣除20(100-20):
3)線程1完成修改工作,將數據版本號加1(version =2),連同賬戶扣除后余額(balance = 50),寫回到內存中。
4)線程2完成了操作,也將版本號加1(version=2)試圖向內存中提交數據(balance=80),但此時對比版本發現,線程2提交的數據版本號為2,數據庫記錄的當前版本也為2,不滿足“提交版本必須大于記錄當前版本才能執行更新”的樂觀鎖策略,就認為這次操作失敗。
讀寫鎖
多線程之間,數據的讀取方之間不會產生線程安全問題,但數據的寫入方相互之間以及和讀者之間都需要進行互斥。如果兩種場景下都用同一個鎖,就會產生極大的性能損耗。所以通過利用讀寫鎖,避免有時反復加鎖,這樣可以減少性能的損耗。
讀寫鎖(readers-writer lock),看英文可以知道,在執行加鎖操作的時候需要額外表明讀寫意圖,不同讀者之間并不互斥,而寫者則要求與任何人互斥。
一個線程對于數據的訪問,主要存在兩種操作:讀數據和寫數據。
- 兩個線程都只是讀一個數據,此時并沒有線程安全問題。直接并發的讀取即可。
- 兩個線程都要寫一個數據,有線程安全問題。
- 一個線程讀另外一個線程寫,這樣也有線程安全問題。
讀寫鎖就是把讀操作和寫操作區分對待,Java標準庫提供了ReentrantReadWreiteLock類,實現了讀寫鎖。
- ReentrantReadWreiteLock.ReadLcok類表示一個讀鎖,這個對象提供了lock/unlock方法進行加鎖操作。
- ReentrantReadWreiteLock.WriteLock類表示一個寫鎖,這個對象也提供了lock/unlock方法進行加鎖操作。
其中,
- 讀加鎖和讀加鎖之間不互斥,讀的時候能讀但不能寫。
- 寫加鎖和寫加鎖之間互斥,寫的時候不能寫。
- 讀加鎖和寫加鎖之間互斥,讀的時候不能寫,寫的時候不能讀。
注意:只要是涉及到“互斥”,就會產生線程的掛起等待,一旦線程掛起,再次喚醒就不知道隔了多久了。
因此盡可能減少“互斥”的機會,就是提高效率的重要途徑。
其中,讀寫鎖特別適合于“頻繁讀,不頻繁寫”的場景中(這樣的場景其實也就是非常廣泛存在的)。
重量級鎖VS輕量級鎖
鎖的核心特性“原子性”,這樣的機制追根溯源是CPU這樣的硬件設備提供的,是層層遞進的。
- CPU提供了“原子操作指令”。
- 操作系統基于CPU的原子指令,實現了mutex互斥鎖。
- jvm基于操作系統提供的互斥鎖,實現了synchronized和ReentrantLock等關鍵字和類。
如下圖:
?可以看到,synchronized并不僅僅是對mutex進行封裝,在synchronized內部還做了很多其它的工作。
重量級鎖:加鎖機制重度依賴了OS提供了mutex
- 大量的內核態用戶態切換
- 很容易引發線程的調度
這兩個操作成本比較高,一旦涉及到用戶態和內核態的切換,就意味著資源消耗和系統狀態變化是非常巨大和復雜的。
輕量級鎖:加鎖機制盡可能不使用mutex,而是盡量在用戶態代碼完成,實在搞不定了,再使用mutex。
- 少量的內核態用戶切換。
- 不太容易引發線程調度。
理解用戶態VS內核態
想象去銀行辦業務。
在窗外,自己做,這是用戶態。用戶態的時間成本是比較可控的。
在窗口內,工作人員做,這是內核態,內核態的時間成本是不太可控的。
如果辦業務的時候反復和工作人員溝通,還需要重新排隊,這時效率是很低的。
?總結:
輕量級鎖,鎖的開銷比較小;重量級鎖,鎖的開銷比較大。
樂觀鎖所預測沖突的概率小,少做了一些工作,通常也就是輕量級鎖;悲觀鎖,所預測沖突的概率大,多做了一些工作,通常也就是重量級鎖。
synchronized開始是一個輕量級鎖,如果鎖沖突比較嚴重,就會變成重量級鎖。
自旋鎖(Spin Lock)
按之前的方式,線程在強鎖失敗后進入阻塞狀態,放棄CPU,需要過很久才能被調度。
但實際上,大部分情況下,雖然當前搶鎖失敗,但過不了很久,鎖就會被釋放。沒必要就放棄CPU。這個時候就可以使用自旋鎖來處理這樣的問題。
自旋鎖偽代碼:
while(搶鎖(lock)==失敗){ }
從上面可以看到,如果獲取鎖失敗,立即再嘗試獲取鎖,無限循環,直到獲取到鎖為止。第一次獲取鎖失敗,第二次的嘗試會在極短的時間內到來,這樣會有更快地響應速度。
一旦鎖被其他線程釋放,就能第一時間獲取到鎖。
自旋鎖是一種典型的輕量級鎖的實現方式:
- 優點:沒有放棄CPU。不涉及線程阻塞和調度,一旦鎖被釋放,就能第一時間獲取到鎖。
- 缺點:如果鎖被其他線程持有的時間比較久,這個循環會消耗CPU資源,那么就會持續的消耗CPU資源。(而互斥鎖掛起等待的時間是不消耗CPU的,因為OS會將CPU分配給其他線程使用,掛起期間等待鎖的線程是不會消耗CPU資源的)。
synchronized中的輕量級鎖策略大概率就是通過自旋鎖的方式實現的。
公平鎖VS非公平鎖
假設三個線程A、B、C,A先嘗試獲取鎖,獲取成功,然后B再嘗試獲取鎖,獲取失敗,阻塞等待;然后,C也嘗試獲取鎖,C也獲取失敗,也阻塞等待。
當線程A釋放鎖的時候,會發生啥呢?
- ?公平鎖:遵守“先來后到”,B比C先來的,當A釋放鎖的之后,B就能先于C獲取到鎖。
- 非公平鎖:不遵守“先來后到”,B和C都可能獲取到鎖。
注意:
- 操作系統內部的線程調度可以視為是隨機的。如果不做任何額外的限制,鎖就是非公平鎖。如果要想實現非公平鎖,就需要依賴額外的數據結構,來記錄線程們的先后順序。
- 公平鎖和非公平鎖沒有好壞之分,關鍵還是看適用場景。
synchronized是非公平鎖。
可重入鎖VS不可重入鎖
可重入鎖的字面意思是“可以重新進入的鎖”,即允許同一個線程多次獲取同一把鎖。
比如一個遞歸函數里有加鎖操作,遞歸過程中,這個鎖會阻塞自己嗎?如果不會,那么這個鎖就是可重入鎖(可重入鎖也可以叫做遞歸鎖)。
Java里只要以Reentrant開頭命名的鎖都是可重入鎖,而且JDK提供的所有線程的Lock實現類,包括synchronized關鍵字鎖都是可重入的。
詳細內容可以看這篇博客中的synchronized的可重入部分:https://blog.csdn.net/2302_76435884/article/details/143416705?spm=1001.2014.3001.5501
二、CAS
何為CAS
CAS:全稱Compare and swap,字面意思:“比較并交換”,一個CAS涉及到一下操作:
比如有一個內存,M,現在還有兩個寄存器,A,B,CAS(M,A,B):
- 如果M和A的值相同的話,就把M和B里的值進行交換,同時整個操作返回true。
- 如果M和A的值不同的話,無事發生,同時整個操作返回false。
CAS偽代碼
下面的代碼不是原子的,真實的CAS是一個原子的硬件指令完成的,這個偽代碼只是輔助理解CAS的工作流程。
boolean CAS(address,expectValue,swapValue){if(&address == expectedValue){&address = swapValue;return true;}return false;
}
當多個線程同時對某個資源進行CAS操作,只有一個線程操作成功,但是并不會阻塞其他線程,其他線程只會收到操作失敗的信號。
CAS可以視為是一種樂觀鎖(或者可以理解成CAS是樂觀鎖的一種實現方式)
CAS其實是一個CPU指令,單個CPU指令是原子的。
CAS有哪些應用
1)實現原子類
標準庫中提供了java.util.concurrent.atomic包,里面的類都是基于這種方式來實現的。
典型的就是AtomicInteger 類。其中的getAndIncrement相當于i++操作。
AtomicInteger atomicInteger = new AtomicInteger(0);
//相當于i++
atomicInteger.getAndIncrement();
class AtomicInteger{private int value;public int getAndIncrement(){int oldValue = value;while(CAS(value,oldValue,oldValue+1)!=true)){oldValue = value;}return oldValue;}
}
?
根據上面的代碼,假設兩個線程同時調用getAndIncrement
- 1)兩個線程都讀取value的值到oldValue中。(oldValue是一個局部變量,在棧上,每個線程有自己的棧)
- ?2)線程1先執行CAS操作。由于oldValue和value的值相同,直接進行對value賦值
注意:
- CAS是直接讀寫內存的,而不是操作寄存器的
- CAS的讀內存,比較,寫內存操作是一條硬件指令,是原子的。
- ?3)線程2再執行CAS操作,第一次CAS的時候發現oldValue和value不相等,不能進行賦值,因此需要進入循環。
在循環里重新讀取value的值賦給oldValue
- 4)線程2接下來第二次執行CAS,此時oldValue和value相同,于是直接執行賦值操作。
- ?5)線程1和線程2返回各自的oldValue的值即可。
?通過形如上述代碼就可以實現一個原子類。不需要使用重量級鎖就可以高效的完成多線程的自增操作。
2)實現自旋鎖
基于CAS實現更靈活的鎖,獲取到更多的控制權。
自旋鎖偽代碼:
class SpinLock{private Thread owner = null;public void lock(){//通過CAS看當前鎖是否被某個線程持有//如果這個鎖已經被別的線程持有,那么就自旋等待//如果這個鎖沒有被別的線程持有,那么就把owner設為當前嘗試加鎖的線程while(!CAS(this.owner,null,Thread.currentThread())){}}public void unlock(){this.owner = null;}
}
?CAS的ABA問題
1)什么是ABA問題
ABA的問題:
假設存在兩個線程t1和t2,有一個共享變量num,初始值為A。
接下來,線程t1想使用CAS把num值改成Z,那么就需要
- 先讀取num的值,記錄到oldNum變量中。
- 使用CAS判定當前num的值是否為A,如果為A,就修改成Z。
但是在t1執行這兩個操作之間,t2線程可能吧num的值從A改成B,又從B改成了A
線程t1的CAS是期望num不變就修改,但是num的值已經被t2給改了。只不過又改成A了。這個時候t1究竟是否要更新num的值為Z呢?
?到這一步,t1線程無法區分當前這個變量始終是A,還是經歷了一個變化過程。
就比如買了一個手機,無法判定這個手機是剛出廠的新手機還是被人用舊了的又翻新過的手機。
2)ABA問題引來的bug
大部分情況下,t2線程這樣的一個反復橫跳的改動,對于t1是否修改num是沒有影響的。但是不排除一些特殊情況。
- 銀行存款100,線程1獲取到當前存款值為100,期望更新為50;線程2獲取到當前存款為100,期望更新為50。
- 線程1執行扣款成功,存款被改成50,線程2阻塞等待中。
- 線程2執行之前,該銀行卡號正好被轉賬50,賬戶余額變成100.
- 輪到線程2執行了,發現當前存款為100,和之前讀到的100相同,再次執行扣款操作。
這個時候扣款操作被執行了兩次!!!
解決方案:
給要修改的值,引入版本號。在CAS比較數據當前值和舊值的同時,也要比較版本號是否符合預期。
CAS操作在讀取舊值的同時,也要讀取版本號。
原理:
某一線程修改值后,值變化,版本號加一,該線程要更新內存中的值和版本號,當該線程的版本號大于內存中的版本號時,對內存中的版本號和值進行更新,否則對內存更新失敗。
具體例子可以看本篇博客中的樂觀鎖悲觀鎖的版本號的例子。
在Java標準庫中提供了AtomicStampReference<E>類。這個類可以對某個類進行包裝,在內部就提供了上面描述的版本管理功能。而對于該類不再展開,有需要的可以自行查找文檔了解。
三、synchronized原理
基本特點:
結合上面的鎖策略,我們就可以總結出,synchronized具有以下特性(只考慮jdk1.8):
- 開始時是樂觀鎖,如果鎖沖突頻繁,就轉換悲觀鎖。
- 開始是輕量級鎖實現,如果鎖被持有的時間較長,就轉換成重量級鎖。
- 實現輕量級鎖的時候大概率用到的自旋鎖策略。
- 是一種不公平鎖
- 是一種可重入鎖
- 不是讀寫鎖
加鎖工作過程
JVM將synchronized鎖分為無鎖、偏向鎖、輕量級鎖、重量級鎖狀態。會根據情況,進行依次升級。
?此處只解釋偏向鎖:
第一個嘗試加鎖的線程,優先進入偏向鎖狀態:
- 偏向鎖不是真的加鎖,只是給對象頭中做一個“偏向鎖的標記”,記錄這個鎖屬于哪個線程。
- 如果后續沒有其他線程競爭該鎖,那么就不用進行其他同步操作了(避免了解鎖的開銷)。
- 如果后續有其他線程來競爭該鎖(剛才已經在鎖對象中記錄了當前鎖屬于哪個線程了,很容易識別當前申請鎖的線程是不是之前記錄的線程),那就取消原來的偏向鎖狀態,進入一般的輕量級鎖狀態。
偏向鎖本質上相當于“延遲加鎖”,能不加鎖就不加鎖,盡量來避免不必要的加鎖開銷。
但是該做的標記還是得做的,否則無法區分何時需要真正加鎖。
?其他的優化操作
鎖消除
編譯器+jvm判斷鎖是否可消除,如果可以,就直接消除。
如果在單個線程中使用StringBuffer,此時編譯器就會自動的把synchronized給優化掉
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
?上面代碼的每個append的調用都會涉及加鎖和解鎖,但如果只是在單線程中執行這個代碼,那么這些加鎖解鎖操作是沒有必要的,白白浪費了一些資源開銷。
鎖粗化
一段邏輯中如果出現多次加鎖解鎖,編譯器+jvm會自動進行鎖的粗化。
鎖的粒度:粗和細
實際開發中,使用細粒度鎖,是期望釋放鎖的時候其他線程能使用鎖,減少持有鎖的時間,其他線程等待鎖的時間也減少,從而提高并發執行的效率。
但是,如果粒度細的鎖,被反復進行加鎖解鎖,可能實際效果還不如粒度粗的鎖(涉及到反復的鎖競爭),這種情況下jvm就會自動把鎖粗化,避免頻繁加鎖釋放鎖。
舉個鎖粗化的例子:
方式一:
打電話,交代任務1,掛電話
打電話,交代任務2,掛電話
打電話,交代任務3,掛電話
方式二:
打電話,交代任務1,任務2,任務3,掛電話
顯然,方式二是更高效的方案。
四、Callable接口
Callable的用法
Callable是一個interface,也是一種創建線程的方式。這個接口相當于把線程封裝一個“返回值”,方便程序員借助多線程的方式計算結果。
代碼示例:創建線程計算1+2+3+...+1000,不使用Callable版本
步驟:
- 創建一個類Result,包含一個sum,表示最終結果,lock表示線程同步使用的鎖對象。
- main方法中先創建了Result實例,然后創建了一個線程t,在線程內部計算1+2+3+..+1000。
- 主線程同時使用wait等待線程t計算結束(注意,如果執行到wait之前,線程t已經計算完了,就不必等待了)。
- 當前線程t計算完畢后,通過notify喚醒主線程,主線程再打印結果 。
public class Solution {static class Result{public int sum = 0;public Object lock = new Object();}public static void main(String[] args) throws InterruptedException {Result result =new Result();Thread t = new Thread(){@Overridepublic void run() {int sum = 0;for(int i = 1;i<=1000;i++){sum+=i;}synchronized (result.lock){result.sum =sum;result.lock.notify();}}};t.start();synchronized (result.lock){while(result.sum == 0){result.lock.wait();}System.out.println(result.sum);}}
}
可以看到,上述代碼需要一個輔助類Result,還需要使用一系列的加鎖操作和wait,notify操作,代碼復雜,容易出錯。
使用Callable版本的:
- 創建一個匿名內部類,實現Callable接口,Callable帶有泛型參數,泛型參數表示返回值類型。
- 重寫Callable的call方法,完成累加的過程,直接通過返回值返回計算結果(call是Callable的核心方法)。
- 把callable實例使用FutureTask包裝一下。
- 創建線程,線程的構造方法傳入FutureTask,此時新線程就會執行FutureTask內部的Callable的call方法,完成計算。計算結果就放到了FutureTask對象中。
- 在主線程中調用futureTask.get()能夠阻塞等待新線程計算完畢,并獲取到FutureTask中的結果。
public class Solution {public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i =1;i<=1000;i++){sum+=i;}return sum;}};FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t =new Thread(futureTask);t.start();
//此處的get是獲取到callable里面的返回結果的int result = futureTask.get();System.out.println(result);}
}
可以看到,使用Callable和FutureTask之后,代碼簡化了很多,也不必手動寫線程同步代碼了。
理解Callable:
- Callable和Runnable是相對的,都是描述一個“任務”。Callable描述的是帶有返回值的任務。
- Runnable描述的是不帶返回值的任務。
- Callable通常需要搭配FutureTask來使用。FutureTask用來保存Callable的返回結果。因為Callable往往是在另一個線程中執行的,啥時候執行完并不確定。
- FutureTask就可以負責這個等待結果出來的工作。
理解FutureTask:
想象去吃麻辣燙,當餐點完好后,后廚就開始做了,同時前臺會給你一張“小票”,這個小票就是FutureTask。后面我們可以隨時憑這張小票去查看自己的這份麻辣燙做出來了沒。
五、JUC(java.util.concurrent)常見類
ReentrantLock
可重入互斥鎖,和synchronized類似,都是用來實現互斥效果,保證線程安全的。
ReentrantLock也是可重入鎖。“Reentrant”這個單詞的原意就是“可重入”
ReentrantLock的用法:
- lock():加鎖,如果獲取不到鎖就死等。
- trylock(超時時間):加鎖,如果獲取不到鎖,等待一定的時間之后就放棄鎖。
- unlock():解鎖.
ReentrantLock lock = new ReentrantLock();
lock.lock();
try{
??????? //working
}finally{
??????? lock.unlock();
}
?ReentrantLock和synchronized的區別:
- synchronized是一個關鍵字,是jvm內部實現的。ReentrantLock是標準庫的一個類,在jvm外實現的(基于Java實現的)。
- synchronized使用時不需要手動釋放鎖;ReentrantLock使用時需要手動釋放,使用起來更靈活,但是也容易遺漏unlock。
- synchronized在申請鎖失敗時,會死等。ReentrantLock可以通過trylock的方式等待一段時間就放棄。
- synchronized是非公平鎖,ReentrantLock默認是非公平鎖,可以通過構造方法傳入一個true開啟公平鎖模式。
//ReentrantLock的構造方法
public ReentrantLock(boolean fair){sync = fair?new FairSync():new NonfairSync();
}
- 強大的喚醒機制。synchronized是通過Object的wait/notify實現等待-喚醒。每次喚醒的是一個隨機等待的線程。ReentrantLock搭配Condition類實現等待-喚醒,可以更精確控制喚醒某個指定的線程。
如何選擇使用哪個鎖?
- 鎖競爭不激烈的時候,使用synchronized,效率更高,自動釋放更方便。
- 鎖競爭激烈的時候,使用ReentrantLock,搭配trylock更靈活控制加鎖行為,而不是死等。
- 如果需要使用公平鎖,使用ReentrantLock。
原子類
上面的內容有講到
線程池
本篇博客中有涉及到 線程池的內容
信號量Semaphore
信號量,用來表示“可用資源的個數”,本質上就是一個計數器。
理解信號量:
- 可以把信號量想象成是停車場的展示牌:當前有車位100個,表示有100個可用資源。
- 當有車開進去的時候,就相當于申請一個可用資源,可用車位就-1(這個稱為信號量的p操作)。
- 當有車開出來的時候,就相當于釋放一個可用資源,可用車位就+1(這個稱為信號量的V操作)。
- 如果計數器的值已經是0了,還嘗試申請資源,就會阻塞等待,直到有其他線程釋放資源。
注意:Semaphore的PV操作中的加減計數器操作都是原子的,可以在多線程環境下直接使用。
代碼示例:
- 創建Semaphore實例,初始化為4,表示有4個可用資源。
- acquire方法表示申請資源(P操作),release方法表示釋放資源(V操作)。
- 創建20個線程,每個線程都嘗試申請資源,sleep1秒之后,釋放資源,觀察程序的執行效果。
public class Solution {public static void main(String[] args) {Semaphore semaphore = new Semaphore(4);Runnable runnable = new Runnable() {@Overridepublic void run() {try {System.out.println("申請資源");semaphore.acquire();System.out.println("我獲取到資源了");Thread.sleep(1000);System.out.println("我釋放資源了");semaphore.release();} catch (InterruptedException e) {e.printStackTrace();}}};for (int i = 0 ;i<20; i++){Thread t = new Thread(runnable);t.start();}}
}
上述程序執行的結果首先會有四個線程搶到資源,而搶不到的線程就會進行阻塞等待,打印“申請資源”,等待一秒之后,搶到資源的線程就開始釋放,阻塞等待的線程就會開始去搶,最后等到所有線程執行完,程序結束。
六、CountDownLatch
CountDownLatch主要適用于多個線程來完成一系列任務的時候,用來衡量任務的進度是否完成。比如需要把一個大的任務,拆分成多個小的任務,讓這些任務并發的去執行,這時就可以使用CountDownLatch來判定說當前這些任務是否全部完成了。
比如好像跑步比賽,10個選手依次就位,哨聲響才同時出發,等到所有選手都通過終點后,才公布成績。
- 構造CountDownLatch實例,初始化10表示有10個任務需要完成。
- 每個任務執行完畢,都調用latch.countDown()。在CountDownLatch內部的計數器同時自減。
- 主線程中使用latch.await();阻塞等待所有任務執行完畢,相當于計數器為0了。
public class Solution {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(10);Runnable r = new Runnable() {@Overridepublic void run() {try{Thread.sleep((long) (Math.random()*10000));latch.countDown();}catch(InterruptedException e){e.printStackTrace();}}};for (int i =0;i<10;i++){new Thread(r).start();}latch.await();System.out.println("比賽結束");}
}
說明:countDown()方法,每次調用這個方法,CountDownLatch內部的計數就會減少1直至減到0。
await方法,阻塞等待所有線程執行完畢后,相當于計數器為0了,最后會喚醒所有在阻塞等待的線程。
以上就是多線程進階部分的內容了,感謝瀏覽!!!
?