Java多線程輪流打印ABC的四種實(shí)現(xiàn)方式
在多線程編程中,一個(gè)經(jīng)典的面試題是:
啟動(dòng)三個(gè)線程,分別打印 A、B、C,要求按順序輪流輸出,如 ABCABCABC… 重復(fù)若干次。
這個(gè)問(wèn)題看似簡(jiǎn)單,但涉及線程間的協(xié)作與通信。本文將從入門(mén)到進(jìn)階,系統(tǒng)講解 4 種主流解法,并附帶完整可運(yùn)行的源碼。
實(shí)現(xiàn)目標(biāo)
假設(shè)每個(gè)線程負(fù)責(zé)打印一個(gè)字符:
- 線程 A 打印 A
- 線程 B 打印 B
- 線程 C 打印 C
期望輸出如下格式:
ABCABCABCABCABC
循環(huán)打印若干次(如 5 次)。
方法一:synchronized + wait/notifyAll
思路解析
使用一個(gè)共享變量 state 表示當(dāng)前輪到哪個(gè)線程。每個(gè)線程進(jìn)入臨界區(qū)后判斷是否是自己該打印的時(shí)機(jī),否則調(diào)用 wait() 掛起,等待喚醒。
示例代碼
/*
1.設(shè)置共享變量state控制線程的執(zhí)行順序
2.在打印方法中傳入兩個(gè)參數(shù):打印的內(nèi)容, 當(dāng)前線程的state
3.每個(gè)線程在調(diào)用打印方法時(shí)都要傳入自己對(duì)應(yīng)的順序,A:0,B:1,C:2
*/
public class PrintABC {
private static int state = 0;// 設(shè)置共享變量控制線程執(zhí)行順序
private static final int COUNT = 5;// 控制執(zhí)行順序
private static Object lock = new Object();// 用于加鎖的對(duì)象
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
printChar("A", 0);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread t2 = new Thread(() -> {
try {
printChar("B", 1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread t3 = new Thread(() -> {
try {
printChar("C", 2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t1.start();
t2.start();
t3.start();
}
private static void printChar(String name, int curState) throws InterruptedException {
int i = 0;
while(i < COUNT) {
synchronized (lock) {
if(state % 3 == curState) {// 是當(dāng)前線程的執(zhí)行順序
System.out.print(name + " ");
i++;// 控制當(dāng)前線程的執(zhí)行的次數(shù)
state++;// 輪轉(zhuǎn)到下一個(gè)線程
lock.notifyAll();
}else {// 輪不到當(dāng)前線程 wait等待
lock.wait();
}
}
}
}
}
優(yōu)缺點(diǎn)
- ? 簡(jiǎn)單易懂,容易上手;
- 使用全局的共享變量state來(lái)控制線程的執(zhí)行順序
- ? 效率較低,
notifyAll()會(huì)喚醒所有線程。
方法二:ReentrantLock + Condition
思路解析
相比 synchronized,ReentrantLock 提供更靈活的線程調(diào)度機(jī)制,Condition 可以精準(zhǔn)喚醒目標(biāo)線程,避免不必要的喚醒。
示例代碼
/*
*第四種方法:ReentrantLock + Condition 來(lái)實(shí)現(xiàn)更加精確的線程間通信
* ReentrantLock實(shí)現(xiàn)加鎖,解鎖; Condition實(shí)現(xiàn)線程間的通信
*/
public static final int COUNT = 5;
public static int state = 0;
public static ReentrantLock lock = new ReentrantLock();
public static Condition conA = lock.newCondition();
public static Condition conB = lock.newCondition();
public static Condition conC = lock.newCondition();
public static void main(String[] args) {
new Thread(() ->printChar("A", 0, conA, conB)).start();
new Thread(() ->printChar("B", 1, conB, conC)).start();
new Thread(() ->printChar("C", 2, conC, conA)).start();
}
private static void printChar(String name, int curState, Condition curCondition, Condition nextCondition) {
for(int i = 0; i < COUNT; i++) {
lock.lock();
try {
while(state % 3 != curState)
curCondition.await();
// 是當(dāng)前線程 執(zhí)行
System.out.print(name);
state++;
nextCondition.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
}
優(yōu)缺點(diǎn)
- ? 支持精準(zhǔn)喚醒,性能優(yōu)于
notifyAll; - ? 編寫(xiě)稍復(fù)雜,需要手動(dòng)釋放鎖。
方法三:Semaphore 信號(hào)量控制
思路解析
使用 3 個(gè)信號(hào)量 semA、semB、semC 控制線程誰(shuí)可以打印,線程執(zhí)行后釋放下一個(gè)信號(hào)量即可。
示例代碼
/*
* 第二種方法:使用Semaphore 信號(hào)量的方式控制執(zhí)行順序
* 如何保證先打印A:semA, semB, semC 將A的許可設(shè)置為1,B,C的許可設(shè)置為0,則一定先執(zhí)行A
* 如何保證打印順序:在打印方法中傳入三個(gè)參數(shù):name, curSem, nextSem A-B B-C C-A
* 執(zhí)行完當(dāng)前線程打印內(nèi)容之后,讓下一個(gè)線程release一個(gè)許可
*/
private static final Semaphore semA = new Semaphore(1);
private static final Semaphore semB = new Semaphore(0);
private static final Semaphore semC = new Semaphore(0);
private static final int COUNT = 5;
public static void main(String[] args) {
new Thread(() -> printChar("A", semA, semB)).start();
new Thread(() -> printChar("B", semB, semC)).start();
new Thread(() -> printChar("C", semC, semA)).start();
}
private static void printChar(String name, Semaphore cur, Semaphore next) {
for(int i = 0; i < COUNT;) {
try {
cur.acquire();
System.out.print(name + " ");
++i;
next.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
優(yōu)缺點(diǎn)
- ? 信號(hào)機(jī)制清晰,邏輯明確;
- ? 不支持靈活的線程增刪;
方法四:BlockingQueue 隊(duì)列傳令
思路解析
為每個(gè)線程分配一個(gè)阻塞隊(duì)列,當(dāng)隊(duì)列有“令牌”時(shí)線程執(zhí)行,執(zhí)行完畢后將令牌交給下一個(gè)隊(duì)列。
阻塞隊(duì)列是一個(gè)線程安全的隊(duì)列
- 隊(duì)列為空時(shí),take會(huì)阻塞
- 隊(duì)列為滿時(shí),put會(huì)阻塞
示例代碼
/*
* 第三種方法:使用BlockingQueue作為令牌的方式來(lái)控制打印順序
* 創(chuàng)建三個(gè)隊(duì)列,每個(gè)隊(duì)列分別打印對(duì)應(yīng)需要打印的內(nèi)容
* 使用令牌來(lái)控制打印的順序,和使用semaphore類(lèi)似
* 在打印的方法中傳入三個(gè)參數(shù):要打印的內(nèi)容,當(dāng)前隊(duì)列,下一個(gè)隊(duì)列
*/
public static BlockingQueue<String> qA = new ArrayBlockingQueue<>(1);
public static BlockingQueue<String> qB = new ArrayBlockingQueue<>(1);
public static BlockingQueue<String> qC = new ArrayBlockingQueue<>(1);
public static final int COUNT = 5;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> printChar("A", qA, qB)).start();
new Thread(() -> printChar("B", qB, qC)).start();
new Thread(() -> printChar("C", qC, qA)).start();
qA.put("go");
}
private static void printChar(String name, BlockingQueue<String> curQueue, BlockingQueue<String> nextQueue) {
for(int i = 0; i < COUNT; i++) {
try {
curQueue.take();// 等待令牌傳遞
System.out.print(name + " ");
nextQueue.put("go");// 傳遞令牌
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
優(yōu)缺點(diǎn)
- ? 隊(duì)列阻塞機(jī)制天然適合線程通信;
- ? 每個(gè)線程都需獨(dú)立隊(duì)列,稍顯繁瑣。
使用Semaphore和BlockingQueue的方式其實(shí)很像;對(duì)于阻塞隊(duì)列來(lái)說(shuō),是通過(guò)傳遞令牌的方式來(lái)交接接力棒
總結(jié)對(duì)比
| 方法 | 控制方式 | 喚醒機(jī)制 | 難度 | 推薦場(chǎng)景 |
|---|---|---|---|---|
| synchronized | 狀態(tài) + 模 3 判斷 | notifyAll | ? | 簡(jiǎn)單測(cè)試、學(xué)習(xí)入門(mén) |
| ReentrantLock | 狀態(tài) + Condition | signal | ?? | 更精確喚醒,推薦實(shí)際開(kāi)發(fā)使用 |
| Semaphore | 信號(hào)量控制順序 | release/acquire | ?? | 控制有限資源訪問(wèn)/固定順序 |
| BlockingQueue | 令牌傳遞 | take/put | ?? | 結(jié)構(gòu)直觀,適合理解通信流程 |
寫(xiě)在最后
線程按順序輪流執(zhí)行是實(shí)際開(kāi)發(fā)中很常見(jiàn)的需求,比如:生產(chǎn)者消費(fèi)者模型、有序打印日志、順序處理任務(wù)等。
掌握以上幾種方法不僅能應(yīng)對(duì)面試題,更能提升對(duì) Java 并發(fā)編程的理解。
以上就是Java多線程輪流打印ABC的四種實(shí)現(xiàn)方式的詳細(xì)內(nèi)容,更多關(guān)于Java多線程輪流打印ABC的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java中對(duì)象的創(chuàng)建和銷(xiāo)毀過(guò)程詳析
這篇文章主要介紹了Java中對(duì)象的創(chuàng)建和銷(xiāo)毀過(guò)程,對(duì)象的創(chuàng)建過(guò)程包括類(lèi)加載檢查、內(nèi)存分配、初始化零值內(nèi)存、設(shè)置對(duì)象頭和執(zhí)行init方法,對(duì)象的銷(xiāo)毀過(guò)程由垃圾回收機(jī)制負(fù)責(zé),文中介紹的非常詳細(xì),需要的朋友可以參考下2025-02-02
Maven項(xiàng)目中引入本地JAR包的兩種常見(jiàn)方式
在開(kāi)發(fā)Java項(xiàng)目時(shí)我們經(jīng)常會(huì)遇到需要引入外部本地jar包的情況,這篇文章主要介紹了Maven項(xiàng)目中引入本地JAR包的兩種常見(jiàn)方式,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-06-06
Spring Boot 整合單機(jī)websocket的步驟 附github源碼
websocket 是一個(gè)通信協(xié)議,通過(guò)單個(gè) TCP 連接提供全雙工通信,這篇文章主要介紹了Spring Boot 整合單機(jī)websocket的步驟(附github源碼),需要的朋友可以參考下2021-10-10
nacos客戶(hù)端一致性hash負(fù)載需求實(shí)現(xiàn)
這篇文章主要介紹了nacos客戶(hù)端一致性hash負(fù)載的需求實(shí)現(xiàn)過(guò)程及步驟詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-02-02
RabbitMQ消費(fèi)者限流實(shí)現(xiàn)消息處理優(yōu)化
這篇文章主要介紹了RabbitMQ消費(fèi)者限流實(shí)現(xiàn)消息處理優(yōu)化,消費(fèi)者限流是用于消費(fèi)者每次獲取消息時(shí)限制條數(shù),注意前提是手動(dòng)確認(rèn)模式,并且在手動(dòng)確認(rèn)后才能獲取到消息,感興趣想要詳細(xì)了解可以參考下文2023-05-05

