本文章收錄于《JAVA并發編程》合集中,本篇來介紹線程間通信,線程間通信 使線程成為一個整體,提高系統之間的 交互性,在提高CPU利用率的同時可以對線程任務進行有效的把控與監督。
比如:多線程之間交替執行,多線程按順序執行等,都需要使用線程通信技術,通過本篇文章您可以獲得:
什么是線程通信,有什么作用
線程通信的三種實現方式
notifyAll的虛假喚醒問題,notify死鎖問題
通過 ReentrantLock 實現精確喚醒
多線程按順序執行的四種方案
線程通信常見面試題解析
相信你還有更多方式實現線程通信?不妨評論區告訴我們吧,高頻率碼字不易,覺得文章不錯記得點贊支持一下哦!
線程間通信
線程之間的交互我們稱之為線程通信【Inter-Thread Communication,簡稱ITC】,指多個線程處理同一資源,但是任務不同
比如:小明放假在家,肚子餓了,如果發現沒有吃的就會喊:媽,我餓了,弄點吃的,如果媽媽發現沒有吃的了就會做菜,通知小明吃飯,總之:有菜通知小明吃飯,沒菜小明通知媽媽做飯,簡直吃貨一個
此時就是兩個線程對飯菜這同一資源有不同的任務,媽媽線程就是做飯,小明線程是吃飯,如果想要實現上邊的場景,就需要媽媽線程和小明線程之間通信
要實現線程之間通信一般有三種方法:
- 使用Object對象鎖的wait()、notify()和notifyAll()方法
- 使用Java5新增的JUC中的ReentrantLock結合Condition
- 使用JUC中的CountDownLatch【計數器】
對象鎖wait和notifyAll方法實現
在此案例中,同一資源就是飯菜,小明對吃的操作是造,而媽媽對吃的操作是做
飯菜資源:
public class KitChenRoom {
// 是否有吃的
private boolean hasFood = false;
// 設置同步鎖,做飯和吃飯只能同時有一個在執行,不能邊做邊吃
private Object lock = new Object();
// 做飯
public void cook() {
// 加鎖
synchronized (lock) {
// 如果有吃的,就不做飯
if(hasFood) {
// 還有吃的,先不做飯
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 否則就做飯,
System.out.println(Thread.currentThread().getName() + "沒吃的了,給娃做飯!");
// 做好之后,修改為true
hasFood = true;
// 通知其他線程吃飯
lock.notifyAll();
}
}
// 吃飯
public void eat() {
synchronized (lock) {
// 如果沒吃的,就喊媽媽做飯,暫時吃不了
if (!hasFood) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 否則就吃飯
System.out.println(Thread.currentThread().getName() + "感謝老媽,恰飯,恰飯");
// 吃完之后,修改為false
hasFood = false;
// 通知其他線程吃飯
lock.notifyAll();
}
}
}
測試類:
public class KitChenMain {
public static void main(String[] args) {
// 創建飯菜對象
KitChenRoom chenRoom = new KitChenRoom();
// 創建媽媽線程,做飯
new Thread(() -> {
for (int i = 0; i < 5; i++) {
chenRoom.cook();
}
},"媽媽線程:").start();
// 創建小明線程,吃飯
new Thread(() -> {
for (int i = 0; i < 5; i++) {
chenRoom.eat();
}
},"小明線程:").start();
}
}
運行結果:發現兩個線程交替執行,沒飯的時候媽媽做飯,有飯的時候小明就恰飯
虛假喚醒
在wait方法的源碼注釋中有這么一段話:
As in the one argument version, interrupts and spurious wakeups are possible,
and this method should always be used in a loop
翻譯:在單參數版本中,中斷和虛假喚醒是可能的,并且該方法應始終在循環中使用
比如上邊的 飯菜資源 代碼中我們使用的是if判斷是否有吃的
如果此時我們再開啟一個大明線程吃飯,開啟一個爸爸線程做飯,此時會發生什么問題呢
改造測試類:再開啟一個大明線程和一個爸爸線程
public class KitChenMain {
public static void main(String[] args) {
KitChenRoom chenRoom = new KitChenRoom();
// 創建媽媽線程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
chenRoom.cook();
}
},"媽媽線程:").start();
// 創建小明線程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
chenRoom.eat();
}
},"小明線程:").start();
// 爸爸線程:做飯
new Thread(() -> {
for (int i = 0; i < 5; i++) {
chenRoom.cook();
}
},"爸爸線程:").start();
// 大明線程:吃飯
new Thread(() -> {
for (int i = 0; i < 5; i++) {
chenRoom.eat();
}
},"大明線程:").start();
}
}
運行結果:發現爸爸線程和媽媽線程連著做了三次飯
原因:
- 這是由于wait方法的機制導致的,wait方法會使線程阻塞,直到被喚醒之后才會運行,在哪里阻塞,再次被喚醒之后得到CPU執行權,就會在哪里繼續運行
- 現在是4條線程,假設爸爸線程運行之后將 hasFood 改為true,此時爸爸線程就會喚醒其他線程,也就是媽媽線程和小明,大明線程都會被喚醒,如果此時媽媽線程獲取到CPU時間片開始運行,判斷 hasFood 為 true,那么就觸發wait等待,等待之后就會釋放CPU執行權,喚醒其他線程
- 如果此時爸爸線程又獲取到CPU執行權,同樣判斷hasFood之后為true,就會進入等待,喚醒其他線程,如果此時CPU執行權又分配給了媽媽線程,因為之前已經經過了判斷,就會在wait的地方,繼續執行,就會觸發給娃做飯,之后再喚醒其他線程
- 此時爸爸線程得到CPU時間片,則會在上次wait的地方繼續執行,同樣的給娃做飯,就會出現上圖的效果,爸媽線程交替做飯
解決:將if替換為while,while語句塊每次執行完之后都會重新判斷,知道條件不成立才會結束循環,即可解決
public class KitChenRoom {
private boolean hasFood = false;
private Object lock = new Object();
public void cook() {
// 加鎖
synchronized (lock) {
// 將if替換為while
while(hasFood) {
// 還有吃的,先不做飯
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 否則就做飯,
System.out.println(Thread.currentThread().getName() + "沒吃的了,給娃做飯!");
// 做好之后,修改為true
hasFood = true;
// 通知其他線程吃飯
lock.notifyAll();
}
}
// 吃飯
public void eat() {
synchronized (lock) {
// 將if替換為while
while (!hasFood) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 否則就吃飯
System.out.println(Thread.currentThread().getName() + "感謝老媽,恰飯,恰飯");
// 吃完之后,修改為false
hasFood = false;
// 通知其他線程吃飯
lock.notifyAll();
}
}
}
運行結果:發現做飯和吃飯交替執行
為什么使用while就能解決呢?其實就是 if和while的區別
由于在多線程內容中,有很多小伙伴犯迷,為什么用while就解決了,其實是思路沒有打開,把以前學的東西都忘記了,滿腦子都是多線程的東西,你說是不是!學習要融會貫通,將前后所有的知識點串起來
解決虛假喚醒非常簡單,其實就是利用了while的特性,while體每次執行都會循環再次判斷條件,直到條件不成立跳出循環,在這也是一樣:
- 媽媽線程執行發現hasFood = true,就進入等待,再次得到cpu時間片執行時,在哪里等待就在哪里醒來繼續執行,也就是再lock.wait()的地方繼續執行
- 由于該代碼在while循環中,會循環判斷,如果hasFood = true繼續wait,如果hasFood = false就跳出循環,執行循環體之外的代碼
- 但是如果是if,就只會判斷一次,醒來之后不會再次判斷,因為lock.wait()代碼已經執行過了,會直接向下執行,開始給娃做飯
notify和notifyAll
上邊我們使用notifyAll喚醒了所有線程,如果將notifyAll替換為notify會發生什么?
public class KitChenRoom {
private boolean hasFood = false;
private Object lock = new Object();
public void cook() {
synchronized (lock) {
while (hasFood) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + "沒吃的了,給娃做飯!");
hasFood = true;
// // 替換為notify
lock.notify();
}
}
public void eat() {
synchronized (lock) {
while (!hasFood) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
hasFood = false;
// 替換為notify
lock.notify();
}
}
}
運行結果:運行三次,發現前兩次程序卡住不動,產生死鎖,第三次正常執行完
在解釋這個原因之前先搞清楚 鎖池 和 等待池 兩個概念:
- 鎖池:假設線程A已經擁有了某個對象的鎖【注意:不是類】,而其它的線程想要調用這個對象的某個synchronized方法【或者synchronized塊】,由于這些線程在進入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權,但是該對象的鎖目前正被線程A擁有,所以這些線程就進入了該對象的鎖池中。
- 等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖,之后進入到了該對象的等待池中
對象鎖:任何一個對象都可以被當做鎖,所以稱為對象鎖,比如下方代碼lock1和lock2就是兩把對象鎖,都有自己獨立的鎖池和等待池
- 調用 lock1.wait() 就是該線程進入到lock1對象鎖的等待池中
- lock1.notify()就是喚醒lock1對象鎖的等待池中的隨機一個等待線程,lock1.notifyAll(); 就是喚醒該等待池中所有等待線程
- lock1的鎖池和等待池與lock2是獨立的,互不影響,并不會喚醒彼此等待池中的線程
// 鎖1
private Object lock1 = new Object();
// 鎖2
private Object lock2 = new Object();
public void cook() {
// 使用lock1對象鎖
synchronized (lock1) {
lock1.wait();
}
lock1.notify();
}
調用wait、notify、notifyAll之后線程變化:
- 如果線程調用了對象的wait()方法,那么線程便會處于該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。
- 當有線程調用了對象的notifyAll()方法【喚醒所有該對象等待池中的 wait 線程】或 notify()方法【只隨機喚醒一個該對象等待池中的 wait 線程】,被喚醒的的線程便會進入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。也就是說,調用了notify后只要一個線程會由等待池進入鎖池,而notifyAll會將該對象等待池內的所有線程移動到鎖池中,等待鎖競爭
- 優先級高的線程競爭到對象鎖的概率大,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中,唯有線程再次調用wait()方法,它才會重新回到等待池中。而競爭到對象鎖的線程則繼續往下執行,直到執行完了 synchronized 代碼塊,它會釋放掉該對象鎖,這時鎖池中的線程會繼續競爭該對象鎖。
為什么會死鎖呢?
KitChenRoom中有 cook 和 eat 兩個方法都是有同步代碼塊,并且進入while之后就會調用lock對象鎖的wait方法,所以多個調用過cook和eat方法的線程就會進入等待池處于阻塞狀態,等待一個正在運行的線程來喚醒它們。下面分別分析一下使用notify和notifyAll方法喚醒線程的不同之處:
- 使用notify:notify方法只能喚醒一個線程,其它等待的線程仍然處于wait狀態,假設調用cook方法的線程執行完后,所有的線程都處于等待狀態,此時又執行了notify方法,這時如果喚醒的仍然是一個調用cook方法的線程【比如爸爸線程 將 媽媽線程喚醒】,那么while循環等于true,則此喚醒的線程【媽媽線程】就會調用wait方法,也會處于等待狀態,而且沒有喚醒其他線程,那就芭比Q了,此時所有的線程都處于等待狀態,就發生了死鎖。
- 使用notifyAll:可以喚醒所有正在等待該鎖的線程,那么所有的線程都會處于運行前的準備狀態(就是cook方法執行完后,喚醒了所有等待該鎖的線程),那么此時,即使再次喚醒一個調用cook方法的線程,while循環等于true,喚醒的線程再次處于等待狀態,那么還會有其它的線程可以獲得鎖,進入運行狀態。
解決wait死鎖的兩種方案:
- 通過調用notifyAll喚醒所有等待線程
- 調用 wait(long timeout) 重載方法,設置等待超時時長,在指定時間內還沒被喚醒則自動醒來
下邊仍然是調用 notify 喚醒等待池中的一個線程,但是調用wait(long timeout) 超時等待方法,讓線程進入等待狀態
public class KitChenRoom {
private boolean hasFood = false;
private Object lock = new Object();
public void cook() {
synchronized (lock) {
while (hasFood) {
try {
// 超時等待 2 秒
lock.wait(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + "沒吃的了,給娃做飯!");
hasFood = true;
lock.notify();
}
}
public void eat() {
synchronized (lock) {
while (!hasFood) {
try {
// 超時等待 2 秒
lock.wait(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + "感謝老媽,恰飯,恰飯");
hasFood = false;
lock.notify();
}
}
}
運行結果:運行三次發現,第一次程序陷入了兩次等待2秒之后程序繼續執行,這就是超時自動喚醒,避免了死鎖
總結:
- notify方法很容易引起死鎖,除非你根據自己的程序設計,確定不會發生死鎖,notifyAll方法則是線程的安全喚醒方法
- 如果程序允許超時喚醒,則可以使用wait(long timeout)方法
- wait(long timeout,int nanou):與 wait(long timeout)相同,不過提供了納秒級別的更精確的超時控制
ReentrantLock結合Condition
Condition是JDK1.5新增的接口,在java.util.concurrent.locks 包中,提供了類似的Object的監視器方法,與Lock配合可以實現等待/通知模式,方法作用在下方源碼中已簡單注釋,想要查看詳細說明,強烈建議看源碼,通過翻譯軟件翻譯一下就行!
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
import java.util.Date;
public interface Condition {
//使當前線程在接到信號或被中斷之前一直處于等待狀態
void await() throws InterruptedException;
// 使當前線程在接到信號之前一直處于等待狀態。【注意:該方法對中斷不敏感】。
void awaitUninterruptibly();
// 使當前線程在接到信號、被中斷或到達指定等待時間之前一直處于等待狀態。
// 返回值表示剩余時間,如果在nanosTimesout之前喚醒,那么返回值 = nanosTimeout - 消耗時間,如果返回值 <= 0 ,則可以認定它已經超時了
long awaitNanos(long nanosTimeout) throws InterruptedException;
// 使當前線程在接到信號、被中斷或到達指定等待時間之前一直處于等待狀態
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 使當前線程在接到信號、被中斷或到達指定最后期限之前一直處于等待狀態。如果沒有到指定時間就被通知,則返回true,否則表示到了指定時間,返回返回false
boolean awaitUntil(Date deadline) throws InterruptedException;
// 喚醒一個等待線程。該線程從等待方法返回前必須獲得與Condition相關的鎖。
void signal();
// 喚醒所有等待線程。能夠從等待方法返回的線程必須獲得與Condition相關的鎖
void signalAll();
}
在此我們通過經典的生產者消費者案例說一下Condition實現線程通信,多幾種案例思維更寬闊,多樣化理解對技術刺激更大
案例:有一個快遞點,可以接貨和送貨,最多存放5個包裹,再放就提示包裹已滿,派件時包裹送完就不能再送,提示沒有包裹,不能派送
快遞點:
package com.stt.thread.communication;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* 快遞點:
* goodsNumber: 快遞數量,默認為0,最多5個,保障原子性使用 AtomicInteger
* receiving() : 收貨方法,累加貨物數量,每次 + 1
* dispatch() : 派送方法,遞減數量,每次 - 1
* 注意:因為使用 Condition 實現,Condition 需要通過 ReentrantLock 獲取,
* 所以可以使用 ReentrantLock實現同步就不需要 synchronized
*/
public class ExpressPoint {
// 快遞數量,使用原子類
private AtomicInteger goodsNumber = new AtomicInteger();
// 鎖對象
private ReentrantLock lock = new ReentrantLock();
// 創建線程通信對象
private Condition condition = lock.newCondition();
// 收貨方法,使用Lock鎖,就不需要synchronized同步了
public void receiving() {
// 上鎖
lock.lock();
// 寫try...finally,保障無論是否發生異常都可以解鎖,避免死鎖
try {
// 如果達到5個,就提示,并且等待
while (goodsNumber.get() == 5) {
System.out.println("庫房已滿,已不能再接收!");
// 等待,有異常拋出
condition.await();
}
System.out.println(Thread.currentThread().getName() + "已收到編號:" + goodsNumber.incrementAndGet() + "的包裹");
// 喚醒其他線程
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 解鎖
lock.unlock();
}
}
// 派送方法
public void dispatch() {
// 上鎖
lock.lock();
try {
// 等于0就不能再派送
while (goodsNumber.get() == 0) {
System.out.println("沒有包裹,不能派送!");
condition.await();
}
System.out.println(Thread.currentThread().getName() + "已送出編號:" + goodsNumber.get() + "的包裹");
goodsNumber.decrementAndGet();
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 解鎖
lock.unlock();
}
}
}
測試類:通過while死循環,不斷接貨和送貨
public class ExpressPointMain {
public static void main(String[] args) {
ExpressPoint expressPoint = new ExpressPoint();
// 收貨線程
new Thread(() -> {
while (true){
expressPoint.receiving();
}
},"收貨員").start();
// 送貨線程
new Thread(() -> {
while (true){
expressPoint.dispatch();
}
},"送貨員").start();
}
}
運行結果:發現收貨員線程和送貨員線程交替執行,并且庫存滿和送完之后都有對應的提示
總結:在Condition中,用await()替換wait(),用signal()替換 notify(),用signalAll()替換notifyAll(),對于我們以前使用傳統的Object方法,Condition都能夠給予實現
Condition 精準喚醒
不同的 Condition 可以用來等待和喚醒不同的線程,類似于上邊我們說的等待池,但是Condition是通過隊列實現等待和喚醒,Condition的await()方法,會使得當前線程進入等待隊列并釋放鎖,同時線程狀態變為等待狀態。當從await()返回時,當前線程一定是獲取了Condition相關聯的鎖。Condition實現方式在后邊我們再分析
上邊調用await 和 signalAll方法是控制所有該Condition對象的線程,我們有兩個線程分別為收貨和送貨,我們可以創建兩個Condition對象來精準控制等待和喚醒收貨和送貨線程。
package com.stt.thread.communication;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
定義兩個 Condition 對象,一個控制收貨線程等待和喚醒,一個控制送貨線程的等待和喚醒
*/
public class ExpressPoint {
// 快遞數量,使用原子類
private AtomicInteger goodsNumber = new AtomicInteger();
// 鎖對象
private ReentrantLock lock = new ReentrantLock();
// 創建線程通信對象
private Condition receivingCondition = lock.newCondition();
private Condition dispatchCondition = lock.newCondition();
// 收貨方法,使用Lock鎖,就不需要synchronized同步了
public void receiving() {
// 上鎖
lock.lock();
// 寫try...finally,保障無論是否發生異常都可以解鎖,避免死鎖
try {
// 判斷是否繼續接貨
while (goodsNumber.get() == 5) {
System.out.println("庫房已滿,已不能再接收!");
// 讓收貨線程進入等待
receivingCondition.await();
}
System.out.println(Thread.currentThread().getName() + "已收到編號:" + goodsNumber.incrementAndGet() + "的包裹");
// 僅僅喚醒送貨線程
dispatchCondition.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 解鎖
lock.unlock();
}
}
// 派送方法
public void dispatch() {
// 上鎖
lock.lock();
try {
// 判斷是否繼續送貨
while (goodsNumber.get() == 0) {
System.out.println("沒有包裹,不能派送!");
// 送貨線程等待
dispatchCondition.await();
}
System.out.println(Thread.currentThread().getName() + "已送出編號:" + goodsNumber.get() + "的包裹");
goodsNumber.decrementAndGet();
// 喚醒收貨線程
receivingCondition.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 解鎖
lock.unlock();
}
}
}
運行結果:運行結果是一樣的,只是僅僅會讓對應的線程等待和喚醒
Condition實現分析
等待隊列
Conditiont的等待隊列是一個FIFO隊列,隊列的每個節點都是等待在Condition對象上的線程的引用,該線程就是在Condition對象上等待的線程,如果一個線程調用了Condition.await(),那么該線程就會釋放鎖,構成節點加入等待隊列并進入等待狀態。
從下圖可以看出來Condition擁有首尾節點的引用,而新增節點只需要將原有的尾節點nextWaiter指向它,并更新尾節點即可。上述節點引用更新過程沒有使用CAS機制,因為在調用await()的線程必定是獲取了鎖的線程,該過程由鎖保證線程的安全。
一個Lock(同步器)擁有一個同步隊列和多個等待隊列:
如上邊的例子:就是擁有receivingCondition 和 dispatchCondition兩個等待隊列
private Condition receivingCondition = lock.newCondition();
private Condition dispatchCondition = lock.newCondition();
等待
調用Condition的await()方法,會使得當前線程進入等待隊列并釋放鎖,同時線程狀態變為等待狀態。當從await()返回時,當前線程一定是獲取了Condition相關聯的鎖。
線程觸發await()這個過程可以看作是同步隊列的首節點【當前線程肯定是成功獲得了鎖,才會執行await方法,因此一定是在同步隊列的首節點】移動到了Condition的等待隊列的尾節點,并釋放同步狀態進入等待狀態,同時會喚醒同步隊列的后繼節點
喚醒
- 調用signal():會喚醒再等待隊列中的首節點,該節點也是到目前為止等待時間最長的節點
- 調用signalAll():將等待隊列中的所有節點全部喚醒,相當于將等待隊列中的每一個節點都執行一次signal()
CountDownLatch
Java5之后在 java.util.concurrent 也就是【JUC】包中提供了很多并發編程的工具類,如 CountDownLatch 計數器是基于 AQS 框架實現的多個線程之間維護共享變量的類
使用場景
可以通過 CountDownLatch 使當前線程阻塞,等待其他線程完成給定任務,比如,等待線程完成下載任務之后,提示用戶下載完成;導游等待所有游客參觀完之后去下一個景點等
使用介紹
CountDownLatch的構造函數接收一個int類型的參數作為計數器,如果你想等待n個點完成,這里就傳入n。這里所說的n個點,可以是n個線程,也可以是1個線程里的n個執行步驟。CountDownLatch 構造函數如下:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
計數器參數count必須大于等于0,等于0的時候,調用await方法時不會阻塞當前線程。
當我們調用CountDownLatch的countDown()方法時,n就會減1,CountDownLatch的await()方法會阻塞當前線程,直到n變成零,繼續執行。
CountDownLatch 方法
- await():阻塞當前線程,直到計數器為零為止
- await(long timeout, TimeUnit unit):await()的重載方法,可以指定阻塞時長
- countDown():計數器減1,如果計數達到零,釋放所有等待的線程
- getCount(): 返回當前計數
案例:比如開一把英雄聯盟,需要10個人加載完成才會進入游戲,可以理解為10個線程運行完畢之后進入游戲頁面
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
public class LoadingGame {
public static void main(String[] args) {
// 計數器
CountDownLatch latch = new CountDownLatch(10);
// 玩家數組
String[] player = new String[10];
// 隨機數,用來加載進度條時線程睡眠使用,防止直接加載到100
Random random = new Random();
// 循環開啟10個線程,即10個玩家
for (int i = 0; i < 10; i++) {
// 記錄玩家在數組中的下標
int index = i;
new Thread(() -> {
// 循環進度條到100
for (int j = 0; j <= 100; j++) {
try {
// 每加載 1% 就隨機睡眠一段時間
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 修改指定玩家進度條
player[index] = j +"%";
// 輸出當前所有的玩家進度
System.out.print("r" + Arrays.toString(player));
}
// 每加載完一個玩家計數-1
latch.countDown();
}).start();
}
try {
// 阻塞當前線程【main線程】,等待十個玩家加載結束后喚醒
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("n"+"游戲開始");
}
}
運行結果:發現主線程等待10個子線程加載到100時才執行
高頻面試題——如何保證多個線程按順序執行
其實就是讓線程按照指定的順序一個一個執行,這里結合同一案例給大家介紹4種方法:
案例:老師布置作業之后,學生開始寫作業,學生寫完作業老師批改,之后老師再將學生的作業情況記錄下來,這個順序不可錯亂
Thread的join方法
public class HomeworkJoin {
public static void main(String[] args) {
// 布置作業線程
Thread t1 = new Thread(() -> {
System.out.println("......老師布置作業......");
});
// 學生寫作業,需要等待老師布置完
Thread t2 = new Thread(() -> {
try {
// t1插入執行,也就是插隊
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("......學生寫作業......");
});
// 學生寫作業,需要等待老師布置完
Thread t3 = new Thread(() -> {
try {
// t2插隊
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("......老師檢查作業......");
});
// 學生寫作業,需要等待老師布置完
Thread t4 = new Thread(() -> {
try {
// t3插隊
t3.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("......老師記錄作業情況......");
});
// 開啟線程
t1.start();
t2.start();
t3.start();
t4.start();
// t1線程插隊
try {
t4.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("......作業布置和檢查結束......");
}
}
運行結果:
使用Condition(條件變量)
我們可以使用Condition精確喚醒下一個需要執行的線程
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HomeworkCondition {
// 鎖對象
private static Lock lock = new ReentrantLock();
// 阻塞隊列
private static Condition doWork = lock.newCondition();
private static Condition checkWork = lock.newCondition();
private static Condition recordWork = lock.newCondition();
/**
* 為什么要加這三個標識狀態?
* 如果沒有狀態標識,線程就無法正確喚醒,就一直處于等待狀態
*/
private static Boolean t1Run = false;
private static Boolean t2Run = false;
private static Boolean t3Run = false;
public static void main(String[] args) {
// 布置作業線程
Thread t1 = new Thread(() -> {
lock.lock();
try {
System.out.println("......老師布置作業......");
// t1執行完畢
t1Run = true;
// 喚醒doWork等待隊列中的第一個線程
doWork.signal();
}finally {
lock.unlock();
}
});
// 學生寫作業,需要等待老師布置完
Thread t2 = new Thread(() -> {
lock.lock();
try {
// 判斷是否布置作業
if(!t1Run) {
// 還沒布置作業,先不寫作業,進入等待隊列
doWork.await();
}
System.out.println("......學生寫作業......");
t2Run = true;
// 喚醒checkWork等待隊列第一個線程
checkWork.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
});
// 老師家查作業,需要學生寫完
Thread t3 = new Thread(() -> {
lock.lock();
try {
// 判斷學生是否寫完作業
if(!t2Run) {
// 沒寫完,先不檢查,進入等待隊列
checkWork.await();
}
System.out.println("......老師檢查作業......");
t3Run = true;
// 喚醒recordWork等待隊列第一個線程
recordWork.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
});
// 老師上傳作業情況,需要檢查完
Thread t4 = new Thread(() -> {
lock.lock();
try {
if(!t3Run) {
recordWork.await();
}
System.out.println("......老師記錄作業情況......");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}
使用CountDownLatch(倒計數)
聲明三個 CountDownLatch 計數器,初始只都為 1,每次執行上一部操作之后下一步操作的計數器 -1,當計數器值為0時就繼續執行,否則就陷入等待
import java.util.concurrent.CountDownLatch;
public class HomeworkCountDownLatch {
public static void main(String[] args) {
// 創建三個計數器
CountDownLatch doWork = new CountDownLatch(1);
CountDownLatch checkWork = new CountDownLatch(1);
CountDownLatch recordWork = new CountDownLatch(1);
// 布置作業線程
Thread t1 = new Thread(() -> {
System.out.println("......老師布置作業......");
// 布置作業之后,做作業計數器 -1
doWork.countDown();
});
// 學生寫作業,需要等待老師布置完
Thread t2 = new Thread(() -> {
try {
doWork.await();
System.out.println("......學生寫作業......");
// 對 檢查作業 -1
checkWork.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
// 學生寫作業,需要等待老師布置完
Thread t3 = new Thread(() -> {
try {
doWork.await();
System.out.println("......老師檢查作業......");
// 對 錄入作業情況 -1
recordWork.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
// 學生寫作業,需要等待老師布置完
Thread t4 = new Thread(() -> {
try {
recordWork.await();
System.out.println("......老師記錄作業情況......");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}
使用CyclicBarrier(回環柵欄)
CyclicBarrier可以實現讓一組線程等待至某個狀態之后再全部同時執行,【回環】是因為當所有等待線程都被釋放以后,CyclicBarrier可以被重用,可以把這個狀態當做barrier,當調用await()方法之后,線程就處于barrier了。示例如下:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class HomeworkCyclicBarrier {
public static void main(String[] args) {
CyclicBarrier doWork = new CyclicBarrier(2);
CyclicBarrier checkWork = new CyclicBarrier(2);
CyclicBarrier recordWork = new CyclicBarrier(2);
// 布置作業線程
Thread t1 = new Thread(() -> {
try {
System.out.println("......老師布置作業......");
//放開柵欄1
doWork.await();
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
// 學生寫作業,需要等待老師布置完
Thread t2 = new Thread(() -> {
try {
//放開柵欄1
doWork.await();
System.out.println("......學生寫作業......");
//放開柵欄2
checkWork.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
});
// 學生寫作業,需要等待老師布置完
Thread t3 = new Thread(() -> {
try {
//放開柵欄2
checkWork.await();
System.out.println("......老師檢查作業......");
//放開柵欄3
recordWork.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
});
// 學生寫作業,需要等待老師布置完
Thread t4 = new Thread(() -> {
try {
//放開柵欄3
recordWork.await();
System.out.println("......老師記錄作業情況......");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}
此四種方法都可以實現同樣的效果,當然你也可以使用Object的wait() 和 notify()/notifyAll()實現
高頻面試題——Thread.join()和CountDownLatch的區別
- Thread.join()是Thread類的一個方法,Thread.join()的實現是依靠Object的wait()和notifyAll()來完成的,而CountDownLatch是JUC包中的一個工具類
當我們使用ExecutorService 【線程池】,就不能使用join,必須使用CountDownLatch,比如:
ExecutorService service = Executors.newFixedThreadPool(5);
final CountDownLatch latch = new CountDownLatch(5);
for(int x = 0; x < 5; x++) {
service.submit(new Runnable() {
public void run() {
// do something
latch.countDown();
}
});
}
latch.await();
- 調用join方法需要等待thread執行完畢才能繼續向下執行,而CountDownLatch只需要檢查計數器的值為零就可以繼續向下執行,相比之下,CountDownLatch更加靈活一些,可以實現一些更加復雜的業務場景。
為什么wait, notify和notifyAll這些方法在Object類中不在Thread類里面?
Java提供的鎖是對象級的而不是線程級的,線程為了進入臨界區【也就是同步塊內】,需要獲得鎖并等待鎖可用,它們并不知道也不需要知道哪些線程持有鎖,它們只需要知道當前資源是否被占用,是否可以獲得鎖,所以鎖的持有狀態應該由同步監視器來獲取,而不是線程本身。
如果Java不提供關鍵字來解決線程之間的通信,鎖是對象級別,由于wait,notify,notifyAll都是鎖級別的操作,每個對象都可以當做鎖所以把他們定義在Object類中是最合適的。?