Java并發(fā)線程間的通信最佳實(shí)踐
在多線程編程中,線程間通信是一個(gè)核心話題。當(dāng)多個(gè)線程需要協(xié)同完成某個(gè)任務(wù)時(shí),它們必須能夠互相通知狀態(tài)的變化,以避免競態(tài)條件和無效的資源占用。Java提供了多種線程間通信的方式,從最基礎(chǔ)的 wait/notify 機(jī)制,到 Lock 配合 Condition 的靈活方案。本文將帶你全面了解線程間通信的原理、常見陷阱以及如何優(yōu)雅地實(shí)現(xiàn)線程協(xié)作。
一、線程間通信的必要性
思考一個(gè)簡單的場景:兩個(gè)線程操作一個(gè)共享變量,一個(gè)線程負(fù)責(zé)加1,另一個(gè)線程負(fù)責(zé)減1,要求交替執(zhí)行10輪。如果沒有通信機(jī)制,線程A可能連續(xù)加多次,線程B才減一次,導(dǎo)致結(jié)果混亂。線程間通信正是為了解決這類問題——讓線程在合適的時(shí)機(jī)暫停和喚醒,從而保證操作的順序性和數(shù)據(jù)的一致性。
二、傳統(tǒng)的wait/notify機(jī)制
2.1 基本使用
Java中每個(gè)對象都有一組監(jiān)視器方法:wait()、notify()、notifyAll()。它們必須在同步塊(synchronized)中使用,因?yàn)樾枰@取對象的監(jiān)視器鎖。
下面是一個(gè)經(jīng)典的“生產(chǎn)者-消費(fèi)者”示例,兩個(gè)線程交替對變量進(jìn)行+1和-1操作:
class ShareData {
private int number = 0;
public synchronized void increment() throws InterruptedException {
// 1. 判斷
if (number != 0) {
this.wait();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + " => " + number);
// 3. 通知
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (number != 1) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + " => " + number);
this.notifyAll();
}
}
public class WaitNotifyDemo {
public static void main(String[] args) {
ShareData data = new ShareData();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); }
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); }
}
}, "B").start();
}
}運(yùn)行結(jié)果會(huì)交替輸出 A => 1 和 B => 0,共10輪。這里的關(guān)鍵點(diǎn)在于:
- 線程在執(zhí)行操作前,先判斷條件是否滿足(number是否為0或1)。
- 不滿足則調(diào)用
wait()進(jìn)入等待狀態(tài),同時(shí)釋放鎖。 - 操作完成后,調(diào)用
notifyAll()喚醒所有等待的線程。
2.2 虛假喚醒問題
當(dāng)我們將線程數(shù)增加到4個(gè)(兩個(gè)加線程,兩個(gè)減線程),并運(yùn)行多次后,可能會(huì)看到 2、3 等異常值,甚至出現(xiàn)負(fù)數(shù)。這是因?yàn)?nbsp;if 判斷導(dǎo)致的虛假喚醒。
虛假喚醒指的是線程被喚醒后,條件可能已經(jīng)不再滿足,但程序仍然繼續(xù)執(zhí)行。例如,當(dāng) number 為0時(shí),A1和A2都等待在 increment 方法中;當(dāng)B執(zhí)行減1后調(diào)用 notifyAll(),A1和A2同時(shí)被喚醒,它們都從 wait() 后繼續(xù)執(zhí)行,導(dǎo)致 number 被連續(xù)加了兩次,變?yōu)?。
解決方案:將 if 改為 while,使線程被喚醒后重新檢查條件。這是JDK文檔明確要求的。
public synchronized void increment() throws InterruptedException {
while (number != 0) { // 使用while
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + " => " + number);
this.notifyAll();
}2.3 wait/notify的局限性
- 無法精確喚醒:
notifyAll()會(huì)喚醒所有等待線程,增加了不必要的上下文切換;notify()只喚醒一個(gè),但無法指定喚醒哪一個(gè)。 - 必須與synchronized綁定:只能配合
synchronized使用,不夠靈活。 - 無法響應(yīng)中斷:
wait()會(huì)拋出InterruptedException,但線程無法在等待期間主動(dòng)中斷。
三、Lock + Condition:更靈活的通信方式
從JDK 1.5開始,java.util.concurrent.locks 包提供了 Lock 接口和 Condition 接口,彌補(bǔ)了 wait/notify 的不足。
3.1 Condition的基本用法
每個(gè) Condition 對象都相當(dāng)于一個(gè)“隊(duì)列”,通過 await() 和 signal()/signalAll() 實(shí)現(xiàn)線程的等待與喚醒。與 wait/notify 類似,使用前必須先獲取對應(yīng)的鎖。
將上面的例子用 ReentrantLock 和 Condition 改寫:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareData {
private int number = 0;
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + " => " + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number != 1) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + " => " + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
}相比 synchronized,Lock 提供了更多控制能力(如 tryLock、可中斷鎖等),而 Condition 則可以創(chuàng)建多個(gè)等待隊(duì)列,實(shí)現(xiàn)精確喚醒。
3.2 多個(gè)Condition實(shí)現(xiàn)精準(zhǔn)通信
需求:三個(gè)線程 A、B、C 依次執(zhí)行,A 打印5次,B 打印10次,C 打印15次,循環(huán)10輪。
這種場景下,需要在線程A執(zhí)行完后精確喚醒B,B執(zhí)行完后精確喚醒C,C執(zhí)行完后精確喚醒A。通過為每個(gè)線程創(chuàng)建一個(gè) Condition 對象,并結(jié)合一個(gè)狀態(tài)標(biāo)識,即可輕松實(shí)現(xiàn)。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareResource {
private int flag = 1; // 1: A, 2: B, 3: C
private final Lock lock = new ReentrantLock();
private final Condition conditionA = lock.newCondition();
private final Condition conditionB = lock.newCondition();
private final Condition conditionC = lock.newCondition();
public void print5() {
lock.lock();
try {
while (flag != 1) {
conditionA.await();
}
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " => " + i);
}
flag = 2;
conditionB.signal(); // 喚醒B
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
while (flag != 2) {
conditionB.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " => " + i);
}
flag = 3;
conditionC.signal(); // 喚醒C
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
while (flag != 3) {
conditionC.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + " => " + i);
}
flag = 1;
conditionA.signal(); // 喚醒A
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ConditionDemo {
public static void main(String[] args) {
ShareResource resource = new ShareResource();
new Thread(() -> {
for (int i = 0; i < 10; i++) resource.print5();
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) resource.print10();
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) resource.print15();
}, "C").start();
}
}這樣,每個(gè)線程只會(huì)在屬于自己的標(biāo)識位被設(shè)置時(shí)才執(zhí)行,執(zhí)行完后精確喚醒下一個(gè)線程,避免了無效的喚醒競爭。
四、經(jīng)典面試題:交替打印數(shù)字和字母
題目:兩個(gè)線程,一個(gè)打印1~52的數(shù)字,另一個(gè)打印A~Z的字母,要求打印結(jié)果為12A34B...5152Z。
分析:數(shù)字線程每次打印兩個(gè)數(shù)字,字母線程每次打印一個(gè)字母??梢酝ㄟ^一個(gè)標(biāo)志位來控制切換,也可以用 Condition 來實(shí)現(xiàn)精確交替。
4.1 使用 wait/notify 實(shí)現(xiàn)
class Printer {
private int num = 1;
private char letter = 'A';
private boolean printNum = true;
public synchronized void printNumber() {
for (int i = 0; i < 26; i++) {
while (!printNum) {
try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
}
System.out.print(num++);
System.out.print(num++);
printNum = false;
notifyAll();
}
}
public synchronized void printLetter() {
for (int i = 0; i < 26; i++) {
while (printNum) {
try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
}
System.out.print(letter++);
printNum = true;
notifyAll();
}
}
}
public class PrintDemo {
public static void main(String[] args) {
Printer printer = new Printer();
new Thread(printer::printNumber).start();
new Thread(printer::printLetter).start();
}
}4.2 使用 Condition 實(shí)現(xiàn)
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Printer {
private int num = 1;
private char letter = 'A';
private boolean printNum = true;
private final Lock lock = new ReentrantLock();
private final Condition numberCondition = lock.newCondition();
private final Condition letterCondition = lock.newCondition();
public void printNumber() {
lock.lock();
try {
for (int i = 0; i < 26; i++) {
while (!printNum) {
numberCondition.await();
}
System.out.print(num++);
System.out.print(num++);
printNum = false;
letterCondition.signal();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printLetter() {
lock.lock();
try {
for (int i = 0; i < 26; i++) {
while (printNum) {
letterCondition.await();
}
System.out.print(letter++);
printNum = true;
numberCondition.signal();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}五、總結(jié)與最佳實(shí)踐
- 優(yōu)先使用
Lock+Condition- 如果需要精確控制線程喚醒順序、支持中斷或超時(shí),或者需要更靈活的鎖機(jī)制,推薦使用
ReentrantLock和Condition。
- 如果需要精確控制線程喚醒順序、支持中斷或超時(shí),或者需要更靈活的鎖機(jī)制,推薦使用
- 避免虛假喚醒
- 無論使用
wait/notify還是Condition.await(),判斷條件時(shí)必須使用while循環(huán),而不是if。
- 無論使用
- 在
finally中釋放鎖 Lock.unlock()必須放在finally塊中,確保鎖在任何情況下都能被釋放,避免死鎖。- 使用多個(gè)
Condition實(shí)現(xiàn)精確通信- 當(dāng)需要多個(gè)線程協(xié)作時(shí),為每個(gè)線程創(chuàng)建獨(dú)立的
Condition,結(jié)合狀態(tài)標(biāo)志,可以顯著提高代碼的可讀性和效率。
- 當(dāng)需要多個(gè)線程協(xié)作時(shí),為每個(gè)線程創(chuàng)建獨(dú)立的
- 注意
notify()vsnotifyAll()- 使用
Condition.signal()可以精確喚醒一個(gè)等待線程,而signalAll()會(huì)喚醒所有等待該條件的線程。一般情況下,精確喚醒能減少不必要的上下文切換。
- 使用
到此這篇關(guān)于Java并發(fā)線程間的通信最佳實(shí)踐的文章就介紹到這了,更多相關(guān)Java線程間的通信內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)將PDF轉(zhuǎn)為圖片格式的方法詳解
PDF文件和圖片文件,這是兩種完全不一樣的格式,可是有的時(shí)候這兩種格式卻是有相互轉(zhuǎn)換的需要,本文將介紹如何通過Java應(yīng)用程序快速高效地將PDF轉(zhuǎn)為圖片格式。一起來看看吧2023-03-03
Java統(tǒng)計(jì)接口耗時(shí)的六種常見方法
有些小伙伴在工作中,可能經(jīng)常遇到這樣的場景:線上接口突然變慢,用戶抱怨連連,你卻一頭霧水,不知道問題出在哪里,其實(shí),這些問題都離不開一個(gè)基礎(chǔ)技能如何準(zhǔn)確統(tǒng)計(jì)接口耗時(shí),所以本文就跟大家一起聊聊統(tǒng)計(jì)接口耗時(shí)的6種常見方法,需要的朋友可以參考下2025-10-10
多模塊項(xiàng)目使用枚舉配置spring-cache緩存方案詳解
這篇文章主要為大家介紹了多模塊項(xiàng)目使用枚舉配置spring-cache緩存的方案詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
mybatis 在typeAliases別名時(shí)報(bào)錯(cuò)的解決
這篇文章主要介紹了mybatis 在typeAliases別名時(shí)報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
redis.clients.jedis.exceptions.JedisMovedDataException異常解決
redis.clients.jedis.exceptions.JedisMovedDataException?異常是在使用?Jedis?客戶端與?Redis?集群進(jìn)行交互時(shí)發(fā)生的,下面就來介紹一下解決方法,感興趣的可以了解一下2024-05-05
Apache?Maven3.6.0的下載安裝和環(huán)境配置(圖文教程)
本文主要介紹了Apache?Maven3.6.0的下載安裝和環(huán)境配置,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07

