引言:為何并發(fā)編程如此重要?
在現(xiàn)代軟件開發(fā)中,并發(fā)編程已不再是陽春白雪,而是開發(fā)者必備的核心技能之一。隨著多核 CPU 的普及,以及業(yè)務(wù)系統(tǒng)對(duì)高性能、高吞吐量和快速響應(yīng)的持續(xù)追求,充分利用計(jì)算資源、提升程序運(yùn)行效率變得至關(guān)重要。并發(fā)編程正是實(shí)現(xiàn)這一目標(biāo)的關(guān)鍵手段。
什么是并發(fā) (Concurrency)?
并發(fā)指的是在同一個(gè)時(shí)間段內(nèi),能夠處理多個(gè)任務(wù)的能力。注意,這并不意味著多個(gè)任務(wù)在同一時(shí)刻同時(shí)執(zhí)行(那是并行),而是在宏觀上看起來是同時(shí)在推進(jìn)。例如,一個(gè) Web 服務(wù)器需要同時(shí)處理來自多個(gè)用戶的請(qǐng)求,一個(gè) GUI 應(yīng)用需要在響應(yīng)用戶操作的同時(shí)執(zhí)行后臺(tái)計(jì)算,這些都是并發(fā)的典型場(chǎng)景。
為何需要并發(fā)?
-
提升性能:
- 利用多核 CPU: 對(duì)于計(jì)算密集型任務(wù),可以通過并發(fā)將其分解到多個(gè)核心上并行執(zhí)行,縮短總處理時(shí)間。
- 提高資源利用率: 對(duì)于 I/O 密集型任務(wù)(如網(wǎng)絡(luò)請(qǐng)求、磁盤讀寫),當(dāng)一個(gè)任務(wù)等待 I/O 時(shí),CPU 可以切換去執(zhí)行其他任務(wù),避免空閑浪費(fèi)。
- 改善響應(yīng)性: 在圖形用戶界面 (GUI) 或交互式應(yīng)用中,將耗時(shí)操作(如文件下載)放到后臺(tái)線程執(zhí)行,可以保持主線程(UI 線程)的流暢響應(yīng),提升用戶體驗(yàn)。
-
簡(jiǎn)化復(fù)雜問題: 某些問題天然適合分解為多個(gè)并發(fā)執(zhí)行的獨(dú)立單元,使得程序設(shè)計(jì)更模塊化、更易于理解和管理(例如生產(chǎn)者-消費(fèi)者模型)。
image.png
然而,并發(fā)編程并非銀彈,它引入了復(fù)雜性,帶來了諸多挑戰(zhàn),如線程安全問題、死鎖、資源競(jìng)爭(zhēng)等。掌握并發(fā)編程的基礎(chǔ)知識(shí),理解其核心概念、挑戰(zhàn)以及 Java 提供的解決方案,是編寫健壯、高效并發(fā)程序的基石。本文旨在梳理 Java 并發(fā)編程的基礎(chǔ)通識(shí),為讀者構(gòu)建一個(gè)清晰的知識(shí)框架。
一、核心基礎(chǔ)概念
在深入探討 Java 并發(fā)機(jī)制之前,需要先明確幾個(gè)基本概念。
1. 進(jìn)程 (Process) 與線程 (Thread)
- 進(jìn)程: 操作系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位。一個(gè)進(jìn)程是內(nèi)存中一個(gè)正在運(yùn)行的程序?qū)嵗?,它擁有?dú)立的內(nèi)存空間(地址空間)、文件句柄、系統(tǒng)資源等。進(jìn)程間通信 (IPC) 相對(duì)復(fù)雜且開銷較大。
- 線程: 進(jìn)程內(nèi)的一個(gè)執(zhí)行單元,是 CPU 調(diào)度的基本單位。一個(gè)進(jìn)程可以包含多個(gè)線程,它們共享該進(jìn)程的內(nèi)存空間(堆內(nèi)存、方法區(qū)等)和系統(tǒng)資源(如文件句柄),但每個(gè)線程擁有自己獨(dú)立的程序計(jì)數(shù)器、虛擬機(jī)棧和本地方法棧。線程間的通信相對(duì)簡(jiǎn)單高效,但共享資源也帶來了同步問題。
Java 程序啟動(dòng)時(shí)至少會(huì)創(chuàng)建一個(gè)主線程(執(zhí)行 main 方法的線程)。開發(fā)者可以通過 new Thread() 或線程池等方式創(chuàng)建更多線程。
2. 線程的生命周期 (Java specific)
在 Java 中,一個(gè)線程從創(chuàng)建到消亡會(huì)經(jīng)歷不同的狀態(tài),定義在 Thread.State 枚舉中:
-
NEW (新建): 使用
new Thread()創(chuàng)建,但尚未調(diào)用start()方法。 -
RUNNABLE (可運(yùn)行): 調(diào)用
start()方法后,線程進(jìn)入可運(yùn)行狀態(tài)。它包含了操作系統(tǒng)線程狀態(tài)中的 Running (運(yùn)行中) 和 Ready (就緒)。處于此狀態(tài)的線程可能正在 JVM 中執(zhí)行,也可能在等待操作系統(tǒng)分配 CPU 時(shí)間片。
image.png -
BLOCKED (阻塞): 線程等待獲取一個(gè)監(jiān)視器鎖 (Monitor Lock) 時(shí)進(jìn)入此狀態(tài)。通常發(fā)生在進(jìn)入
synchronized方法或代碼塊時(shí),鎖被其他線程持有。當(dāng)獲取到鎖后,會(huì)轉(zhuǎn)換回RUNNABLE。 -
WAITING (無限期等待): 線程等待其他線程執(zhí)行特定動(dòng)作時(shí)進(jìn)入此狀態(tài)。常見觸發(fā)條件:
- 調(diào)用無超時(shí)的
Object.wait()。 - 調(diào)用無超時(shí)的
Thread.join()。 - 調(diào)用
LockSupport.park()。
需要被其他線程顯式喚醒(如Object.notify(),Object.notifyAll(),LockSupport.unpark())才能回到RUNNABLE。
- 調(diào)用無超時(shí)的
-
TIMED_WAITING (有限期等待): 與
WAITING類似,但等待有時(shí)間限制。超時(shí)后會(huì)自動(dòng)返回RUNNABLE。常見觸發(fā)條件:- 調(diào)用帶超時(shí)的
Thread.sleep(long millis)。 - 調(diào)用帶超時(shí)的
Object.wait(long timeout)。 - 調(diào)用帶超時(shí)的
Thread.join(long millis)。 - 調(diào)用帶超時(shí)的
LockSupport.parkNanos(),LockSupport.parkUntil()。
- 調(diào)用帶超時(shí)的
-
TERMINATED (終止): 線程的
run()方法執(zhí)行完畢或因未捕獲的異常退出后,進(jìn)入此狀態(tài)。線程生命周期結(jié)束。
理解線程狀態(tài)對(duì)于調(diào)試并發(fā)問題(如死鎖、性能瓶頸)非常有幫助。
3. 并發(fā) (Concurrency) 與并行 (Parallelism)
- 并發(fā) (Concurrency): 指邏輯上同時(shí)處理多個(gè)任務(wù)的能力。在單核 CPU 上,通過時(shí)間片輪轉(zhuǎn)快速切換任務(wù),使得宏觀上感覺任務(wù)在同時(shí)進(jìn)行。
- 并行 (Parallelism): 指物理上同時(shí)執(zhí)行多個(gè)任務(wù)的能力。這需要多核 CPU 或分布式系統(tǒng)的支持,多個(gè)任務(wù)在同一時(shí)刻真正地一起運(yùn)行。
關(guān)系: 并行是并發(fā)的一種實(shí)現(xiàn)方式。并發(fā)的目標(biāo)是充分利用資源、提高響應(yīng),可以在單核或多核上實(shí)現(xiàn);而并行必須依賴多核或多機(jī)才能實(shí)現(xiàn),是提升絕對(duì)速度的關(guān)鍵。Java 并發(fā)編程既可以用于實(shí)現(xiàn)并發(fā)(提高資源利用率和響應(yīng)性),也可以用于實(shí)現(xiàn)并行(利用多核加速計(jì)算)。
二、并發(fā)編程的三大挑戰(zhàn)
編寫正確的并發(fā)程序之所以困難,主要源于以下三個(gè)核心問題:
1. 原子性 (Atomicity) 問題
原子性指一個(gè)或多個(gè)操作,要么全部執(zhí)行且執(zhí)行過程不被任何因素打斷,要么就都不執(zhí)行。
在并發(fā)環(huán)境中,如果一個(gè)操作不是原子的,那么在執(zhí)行過程中可能被其他線程干擾,導(dǎo)致數(shù)據(jù)不一致,這就是競(jìng)爭(zhēng)條件 (Race Condition)。
經(jīng)典示例: count++ 操作。
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 非原子操作
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join(); // 等待 t1 結(jié)束
t2.join(); // 等待 t2 結(jié)束
System.out.println("Final count: " + counter.getCount()); // 結(jié)果通常小于 20000
}
}

count++ 看似簡(jiǎn)單,實(shí)際包含三個(gè)步驟:
- 讀取
count的當(dāng)前值。 - 將值加 1。
- 將新值寫回
count。
如果兩個(gè)線程同時(shí)執(zhí)行 increment(),可能發(fā)生以下情況:
- 線程 A 讀取
count(假設(shè)為 10)。 - 線程 B 讀取
count(也為 10)。 - 線程 A 計(jì)算 10 + 1 = 11。
- 線程 B 計(jì)算 10 + 1 = 11。
- 線程 A 將 11 寫回
count。 - 線程 B 將 11 寫回
count。
兩次 increment() 操作后,count 預(yù)期應(yīng)為 12,但結(jié)果卻是 11。這就是原子性被破壞導(dǎo)致的競(jìng)爭(zhēng)條件。
解決方案: 需要使用鎖或其他原子操作機(jī)制(如 synchronized, Lock, AtomicInteger)來保證 count++ 的原子性。
2. 可見性 (Visibility) 問題
可見性指當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改。
在現(xiàn)代多核 CPU 架構(gòu)中,每個(gè)核心都有自己的高速緩存 (Cache)。線程對(duì)共享變量的操作可能先在自己的工作內(nèi)存(CPU Cache 的抽象)中進(jìn)行,然后再擇機(jī)寫回主內(nèi)存。這會(huì)導(dǎo)致一個(gè)線程修改了變量值,但該值尚未寫回主內(nèi)存,或者其他線程仍然讀取的是自己緩存中的舊值,從而產(chǎn)生可見性問題。
示例:
public class VisibilityProblem {
private boolean stopRequested = false; // 共享變量
public void requestStop() {
stopRequested = true; // 線程 A 修改
System.out.println("Stop requested.");
}
public void run() {
System.out.println("Worker thread started.");
while (!stopRequested) { // 線程 B 讀取
// do work...
// 可能因?yàn)榭梢娦詥栴},一直讀取到 false,導(dǎo)致循環(huán)無法停止
}
System.out.println("Worker thread stopped.");
}
public static void main(String[] args) throws InterruptedException {
VisibilityProblem worker = new VisibilityProblem();
Thread workerThread = new Thread(worker::run);
workerThread.start();
Thread.sleep(1000); // 讓 worker 線程跑一會(huì)兒
Thread stopperThread = new Thread(worker::requestStop);
stopperThread.start();
// 如果存在可見性問題,workerThread 可能永遠(yuǎn)不會(huì)結(jié)束
workerThread.join(5000); // 等待 worker 線程結(jié)束,設(shè)置超時(shí)
if (workerThread.isAlive()) {
System.out.println("Worker thread did not stop due to visibility issue!");
// 在實(shí)際情況中需要更健壯的停止機(jī)制
}
}
}
在這個(gè)例子中,stopperThread 修改 stopRequested 為 true,但 workerThread 可能由于緩存原因一直讀取到 false,導(dǎo)致死循環(huán)。
解決方案: 使用 volatile 關(guān)鍵字修飾 stopRequested,或使用鎖 (synchronized, Lock) 來保證修改的可見性。

