引言:為何并發編程如此重要?
在現代軟件開發中,并發編程已不再是陽春白雪,而是開發者必備的核心技能之一。隨著多核 CPU 的普及,以及業務系統對高性能、高吞吐量和快速響應的持續追求,充分利用計算資源、提升程序運行效率變得至關重要。并發編程正是實現這一目標的關鍵手段。
什么是并發 (Concurrency)?
并發指的是在同一個時間段內,能夠處理多個任務的能力。注意,這并不意味著多個任務在同一時刻同時執行(那是并行),而是在宏觀上看起來是同時在推進。例如,一個 Web 服務器需要同時處理來自多個用戶的請求,一個 GUI 應用需要在響應用戶操作的同時執行后臺計算,這些都是并發的典型場景。
為何需要并發?
-
提升性能:
- 利用多核 CPU: 對于計算密集型任務,可以通過并發將其分解到多個核心上并行執行,縮短總處理時間。
- 提高資源利用率: 對于 I/O 密集型任務(如網絡請求、磁盤讀寫),當一個任務等待 I/O 時,CPU 可以切換去執行其他任務,避免空閑浪費。
- 改善響應性: 在圖形用戶界面 (GUI) 或交互式應用中,將耗時操作(如文件下載)放到后臺線程執行,可以保持主線程(UI 線程)的流暢響應,提升用戶體驗。
-
簡化復雜問題: 某些問題天然適合分解為多個并發執行的獨立單元,使得程序設計更模塊化、更易于理解和管理(例如生產者-消費者模型)。
image.png
然而,并發編程并非銀彈,它引入了復雜性,帶來了諸多挑戰,如線程安全問題、死鎖、資源競爭等。掌握并發編程的基礎知識,理解其核心概念、挑戰以及 Java 提供的解決方案,是編寫健壯、高效并發程序的基石。本文旨在梳理 Java 并發編程的基礎通識,為讀者構建一個清晰的知識框架。
一、核心基礎概念
在深入探討 Java 并發機制之前,需要先明確幾個基本概念。
1. 進程 (Process) 與線程 (Thread)
- 進程: 操作系統進行資源分配和調度的基本單位。一個進程是內存中一個正在運行的程序實例,它擁有獨立的內存空間(地址空間)、文件句柄、系統資源等。進程間通信 (IPC) 相對復雜且開銷較大。
- 線程: 進程內的一個執行單元,是 CPU 調度的基本單位。一個進程可以包含多個線程,它們共享該進程的內存空間(堆內存、方法區等)和系統資源(如文件句柄),但每個線程擁有自己獨立的程序計數器、虛擬機棧和本地方法棧。線程間的通信相對簡單高效,但共享資源也帶來了同步問題。
Java 程序啟動時至少會創建一個主線程(執行 main
方法的線程)。開發者可以通過 new Thread()
或線程池等方式創建更多線程。
2. 線程的生命周期 (Java specific)
在 Java 中,一個線程從創建到消亡會經歷不同的狀態,定義在 Thread.State
枚舉中:
-
NEW (新建): 使用
new Thread()
創建,但尚未調用start()
方法。 -
RUNNABLE (可運行): 調用
start()
方法后,線程進入可運行狀態。它包含了操作系統線程狀態中的 Running (運行中) 和 Ready (就緒)。處于此狀態的線程可能正在 JVM 中執行,也可能在等待操作系統分配 CPU 時間片。
image.png -
BLOCKED (阻塞): 線程等待獲取一個監視器鎖 (Monitor Lock) 時進入此狀態。通常發生在進入
synchronized
方法或代碼塊時,鎖被其他線程持有。當獲取到鎖后,會轉換回RUNNABLE
。 -
WAITING (無限期等待): 線程等待其他線程執行特定動作時進入此狀態。常見觸發條件:
- 調用無超時的
Object.wait()
。 - 調用無超時的
Thread.join()
。 - 調用
LockSupport.park()
。
需要被其他線程顯式喚醒(如Object.notify()
,Object.notifyAll()
,LockSupport.unpark()
)才能回到RUNNABLE
。
- 調用無超時的
-
TIMED_WAITING (有限期等待): 與
WAITING
類似,但等待有時間限制。超時后會自動返回RUNNABLE
。常見觸發條件:- 調用帶超時的
Thread.sleep(long millis)
。 - 調用帶超時的
Object.wait(long timeout)
。 - 調用帶超時的
Thread.join(long millis)
。 - 調用帶超時的
LockSupport.parkNanos()
,LockSupport.parkUntil()
。
- 調用帶超時的
-
TERMINATED (終止): 線程的
run()
方法執行完畢或因未捕獲的異常退出后,進入此狀態。線程生命周期結束。
理解線程狀態對于調試并發問題(如死鎖、性能瓶頸)非常有幫助。
3. 并發 (Concurrency) 與并行 (Parallelism)
- 并發 (Concurrency): 指邏輯上同時處理多個任務的能力。在單核 CPU 上,通過時間片輪轉快速切換任務,使得宏觀上感覺任務在同時進行。
- 并行 (Parallelism): 指物理上同時執行多個任務的能力。這需要多核 CPU 或分布式系統的支持,多個任務在同一時刻真正地一起運行。
關系: 并行是并發的一種實現方式。并發的目標是充分利用資源、提高響應,可以在單核或多核上實現;而并行必須依賴多核或多機才能實現,是提升絕對速度的關鍵。Java 并發編程既可以用于實現并發(提高資源利用率和響應性),也可以用于實現并行(利用多核加速計算)。
二、并發編程的三大挑戰
編寫正確的并發程序之所以困難,主要源于以下三個核心問題:
1. 原子性 (Atomicity) 問題
原子性指一個或多個操作,要么全部執行且執行過程不被任何因素打斷,要么就都不執行。
在并發環境中,如果一個操作不是原子的,那么在執行過程中可能被其他線程干擾,導致數據不一致,這就是競爭條件 (Race Condition)。
經典示例: 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 結束
t2.join(); // 等待 t2 結束
System.out.println("Final count: " + counter.getCount()); // 結果通常小于 20000
}
}
count++
看似簡單,實際包含三個步驟:
- 讀取
count
的當前值。 - 將值加 1。
- 將新值寫回
count
。
如果兩個線程同時執行 increment()
,可能發生以下情況:
- 線程 A 讀取
count
(假設為 10)。 - 線程 B 讀取
count
(也為 10)。 - 線程 A 計算 10 + 1 = 11。
- 線程 B 計算 10 + 1 = 11。
- 線程 A 將 11 寫回
count
。 - 線程 B 將 11 寫回
count
。
兩次 increment()
操作后,count
預期應為 12,但結果卻是 11。這就是原子性被破壞導致的競爭條件。
解決方案: 需要使用鎖或其他原子操作機制(如 synchronized
, Lock
, AtomicInteger
)來保證 count++
的原子性。
2. 可見性 (Visibility) 問題
可見性指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。
在現代多核 CPU 架構中,每個核心都有自己的高速緩存 (Cache)。線程對共享變量的操作可能先在自己的工作內存(CPU Cache 的抽象)中進行,然后再擇機寫回主內存。這會導致一個線程修改了變量值,但該值尚未寫回主內存,或者其他線程仍然讀取的是自己緩存中的舊值,從而產生可見性問題。
示例:
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...
// 可能因為可見性問題,一直讀取到 false,導致循環無法停止
}
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 線程跑一會兒
Thread stopperThread = new Thread(worker::requestStop);
stopperThread.start();
// 如果存在可見性問題,workerThread 可能永遠不會結束
workerThread.join(5000); // 等待 worker 線程結束,設置超時
if (workerThread.isAlive()) {
System.out.println("Worker thread did not stop due to visibility issue!");
// 在實際情況中需要更健壯的停止機制
}
}
}
在這個例子中,stopperThread
修改 stopRequested
為 true
,但 workerThread
可能由于緩存原因一直讀取到 false
,導致死循環。
解決方案: 使用 volatile
關鍵字修飾 stopRequested
,或使用鎖 (synchronized
, Lock
) 來保證修改的可見性。