1. 什麼是執行緒(thread)
執行緒作為獨立的工作線
在 Java(以及程式設計整體)中,執行緒是彼此獨立的指令序列,能與同一程式中的其他執行緒並行運作。想像一間工廠:每位裁縫都有自己的工作桌與任務,各自獨立工作,但成果匯聚成共同的結果。
預設情況下,Java 程式從單一執行緒開始——也就是啟動 main 方法的那個。不過,我們可以建立額外的執行緒,讓程式的不同部分同時執行。
行程與執行緒:有何差別?
- 行程——「重量級」的執行單位。每個行程有自己的記憶體區域、變數與資源。行程彼此完全隔離——就算其中一個「故障」,其他也不會受影響。
- 執行緒(thread)——行程內的「輕量級」執行單位。同一行程的所有執行緒共享記憶體與資源。這表示它們能輕鬆交換資料(但也可能彼此干擾)。
類比:
行程就像一間獨立的公寓:有自己的牆與住戶。
執行緒就像同一間公寓裡的住戶:各做各的,但共用廚房和浴室。
在 Java 中如何呈現?
當你啟動程式時,JVM 會至少建立一個執行緒——主執行緒(main)。但你可以建立新的執行緒,讓任務平行執行。
2. 為什麼需要多執行緒
介面回應性:UI 不應該「卡住」
假設你在寫一個圖形介面程式——比如文字編輯器。使用者按下「儲存」,而你開始長時間把檔案寫到磁碟。如果一切都在主執行緒中進行,視窗就會「卡住」:使用者無法點選、游標不動、介面沒有反應。如果把儲存動作放到獨立執行緒——介面仍保持回應,使用者甚至可以反悔並關閉程式。
生活化範例:
你打開瀏覽器開始下載大檔。如果瀏覽器不使用執行緒,你在檔案完成下載前,既不能開新分頁,也不能捲動頁面!
平行處理資料
例如,你有一份包含上千個檔案的清單需要處理(像是重新計算雜湊或替換文字)。為何不平行處理?每個執行緒拿一個檔案、各自獨立處理,整體工作就能快上好幾倍。
範例:
伺服器需要處理數百位客戶端的請求。若伺服器只用單一執行緒,其他客戶端可能要等到天荒地老。使用執行緒時,每個請求能被獨立處理!
善用多核心處理器
現代處理器不是只有一顆「大腦」,而是一整隊核心,可同時工作。如果你的程式只使用一個執行緒,其餘核心就會閒著,彷彿在玩踩地雷。啟動多個執行緒——所有核心都有事做,程式執行得更快。
有趣的小知識:
你的手機都有多個核心,筆電與伺服器甚至有數十個!不用滿它們,就好比買了公車卻一個人搭。
3. 生活中的例子
| 領域 | 多執行緒範例 |
|---|---|
| 檔案下載 | 同時下載多個檔案 |
| 使用者介面(UI) | 載入/儲存資料時應用程式不會「卡住」 |
| 伺服器 | 平行處理大量網路請求 |
| 遊戲 | 物理、圖形、音訊、AI 各自使用獨立執行緒 |
| 即時通訊 | 接收訊息、傳送檔案、更新介面 |
| 影像處理 | 逐畫面平行處理 |
小小比喻:
廚師在煮湯、烤箱同時在烤派、掃地機器人在打掃——一切同時進行,晚餐更快就緒!
4. 多執行緒的潛在複雜性
競爭條件(race condition)
當多個執行緒同時修改同一變數時,結果可能不可預期。比如兩個執行緒同時遞增共享計數器,最終值可能不正確。之後的課程我們會更詳細討論。
同步
為了避免執行緒彼此干擾,必須想辦法「協調」——誰在何時可以修改資料。這稱為同步。為此有特殊的關鍵字與結構(synchronized、鎖等),我們稍後會介紹。
Deadlock(互相鎖死)
有時執行緒會彼此「等待」到天荒地老,導致程式卡死。這稱為 deadlock——是多執行緒程式設計中最陰險的錯誤之一。
除錯與測試
多執行緒的錯誤很難捕捉:有時正常、有時不正常。有些錯誤只在伺服器或使用者端出現,而在你的電腦上卻完美無缺。這讓多執行緒程式的測試與除錯成為開發者的真正任務關卡。
5. 快速總覽:多執行緒程式長什麼樣
沒有執行緒的範例:
public class Main {
public static void main(String[] args) {
// 數到 5
for (int i = 1; i <= 5; i++) {
System.out.println(i);
}
// 輸出字母
for (char c = 'A'; c <= 'E'; c++) {
System.out.println(c);
}
}
}
輸出總是一樣:
1
2
3
4
5
A
B
C
D
E
使用執行緒的範例:
public class Main {
public static void main(String[] args) {
Thread numbers = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println(i);
try {
Thread.sleep(100); // 稍微等待一下
} catch (InterruptedException e) {
// 忽略
}
}
});
Thread letters = new Thread(() -> {
for (char c = 'A'; c <= 'E'; c++) {
System.out.println(c);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// 忽略
}
}
});
numbers.start();
letters.start();
}
}
輸出會交錯:
1
A
2
B
3
C
4
D
5
E
或者,如果執行緒「競賽」,順序可能會不同。重點是——兩個迴圈是並行進行的!
6. 實用細節
視覺示意:執行緒如何一起工作
+-------------------+ +-------------------+
| Glavnyy potok | | Vtoroy potok |
+-------------------+ +-------------------+
| 1 | 2 | 3 | 4 | 5 | | A | B | C | D | E |
+-------------------+ +-------------------+
| |
| Oba rabotayut |
| odnovremenno |
+-------------------------+
Java 在哪些地方「底層」使用了執行緒
- 垃圾回收(Garbage Collector)——獨立的執行緒清理未使用的物件。
- 輸入/輸出(I/O)——檔案讀寫、網路連線。
- 伺服器與 Web 應用——每個客戶端請求在獨立執行緒中處理。
- 計時器、排程器——依排程執行任務。
7. 新手常見錯誤
錯誤 1:以為執行緒一定會加速程式。
事實上,如果你的機器是單核心,或工作安排不當,多執行緒反而可能因為「混亂」與執行緒切換的額外開銷而變慢。
錯誤 2:忽視同步問題。
很多人以為:「我就只是啟兩個執行緒,能出什麼事?」但如果兩個執行緒都在修改同一個變數,結果可能完全出乎意料。
錯誤 3:凡事都丟給執行緒。
不要為每個小需求都啟一個獨立執行緒。執行緒是一種資源,數量過多會導致變慢,甚至讓程式崩潰。
錯誤 4:沒有處理錯誤。
執行緒可能會拋出例外(例如處理檔案或網路時)。如果不處理這些錯誤,程式可能會異常終止或「卡住」。
GO TO FULL VERSION