1. Thread 類別:在 Java 中的第一個執行緒
在 Java 中,每個執行緒都由類別 Thread 的物件表示。這個類別就像管弦樂的指揮——它負責控制執行緒的啟動、停止與生命週期。
要啟動一個執行緒,需要:
- 建立一個 Thread 類別(或其子類別)的物件。
- 在該物件上呼叫方法 start()。
我們來實作看看。
範例 1:從 Thread 繼承
最直接的方式是:讓你的類別繼承自 Thread,並覆寫它的 run() 方法。你在 run() 內寫的程式碼會在獨立的執行緒中執行。
// 透過繼承建立的簡單執行緒
public class HelloThread extends Thread {
@Override
public void run() {
System.out.println("執行緒向你問好!我的名字是:" + getName());
}
}
public class Main {
public static void main(String[] args) {
HelloThread thread = new HelloThread(); // 建立執行緒物件
thread.start(); // 啟動執行緒
System.out.println("主執行緒結束工作.");
}
}
實際上發生了什麼?
當你呼叫 thread.start() 時,Java 會建立一個新的執行緒,並在該執行緒裡執行方法 run()。同時,主要執行緒(也就是從 main 開始的那個)不會等待,而是繼續並行執行。
最重要的提醒:不要把 start() 和直接呼叫 run() 混淆。如果寫成 thread.run(),就不會產生任何新執行緒——那只是一般方法,會在你呼叫它的同一個執行緒中執行。真正的多執行緒只會從 start() 開始。
什麼是 getName()?
方法 getName() 會回傳執行緒的名稱。預設情況下,Java 會給執行緒取名為 "Thread-0"、"Thread-1" 等,這對除錯很方便。
2. Runnable 介面:對多數情境最好的方式
Java 追求彈性。類別 Thread 已繼承自 Object,而 Java 不支援類別的多重繼承。如果你的類別繼承了 Thread,就不能再繼承其他類別(例如你有個類別 Car,想把它做成執行緒,就必須做出取捨)。
解法:把執行緒的邏輯抽到一個實作介面 Runnable 的獨立物件。這個介面只有一個方法 run()。接著你建立類別 Thread 的物件,把你的 Runnable 傳給它——然後啟動執行緒。
範例 2:實作 Runnable 的類別
// 實作 Runnable 的類別
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println("來自 Runnable 的問候!執行緒: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Runnable runnable = new HelloRunnable();
Thread thread = new Thread(runnable); // 將 Runnable 傳給 Thread
thread.start();
System.out.println("主執行緒結束工作.");
}
}
在這裡,我們建立了 HelloRunnable 類別,實作了介面 Runnable。在方法 main 中建立這個類別的物件,並把它傳入 Thread 的建構函式。之後透過呼叫 start() 啟動執行緒。
另外值得一提的是方法 Thread.currentThread()。這是個靜態方法,能取得目前執行緒的物件,藉此知道我們的程式碼究竟在哪個執行緒中執行。
為什麼這樣更好?
- 你的類別可以繼承任意其他類別(而不只限於 Thread)。
- 可以重複使用同一個 Runnable 來在多個執行緒中啟動。
- 程式碼更彈性、更乾淨。
3. 語法:匿名類別與 Lambda 運算式
Java 一直在進化!自 Java 8 之後,我們可以寫得更精簡、更好用。
範例 3:匿名類別
有時候只需要用一次的類別,不想另外建立檔案,就可以在使用的地方直接宣告——這就是匿名類別。
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名的 Runnable!執行緒: " + Thread.currentThread().getName());
}
});
thread.start();
System.out.println("主執行緒結束工作.");
}
}
範例 4:Lambda 運算式 (Java 8+)
介面 Runnable 是函式式的(只有一個抽象方法)。因此可以使用 Lambda 運算式:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Lambda 執行緒!執行緒: " + Thread.currentThread().getName());
});
thread.start();
System.out.println("主執行緒結束工作.");
}
}
簡而言之:
Runnable 可以「就地」實作,而 Lambda 運算式則提供方法 run() 的實作。
4. 啟動多個執行緒
你可以啟動很多執行緒(呃,基本上——仍取決於電腦資源)。
範例 5:啟動多個執行緒
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
int threadNumber = i; // 對 Lambda 一定要複製該變數!
Thread thread = new Thread(() -> {
System.out.println("執行緒 #" + threadNumber + ": 嗨!");
});
thread.start();
}
System.out.println("主執行緒結束工作.");
}
}
輸出可能會不同!
執行緒是並行運作的——誰先輸出自己的訊息,取決於作業系統與執行緒排程器。
5. 執行緒的生命週期
了解執行緒如何運作,對除錯與正確使用執行緒非常重要。
執行緒的主要狀態
- NEW —— 執行緒已建立,但尚未啟動(new Thread(...))。
- RUNNABLE —— 執行緒已啟動(start()),可以執行。
- TERMINATED —— 執行緒已結束執行(方法 run() 已結束)。
呼叫 start() 與 run() 時會發生什麼
- start() —— 建立新執行緒,並在該執行緒中呼叫其方法 run()。
- run() —— 一般方法;若直接呼叫,會在目前執行緒中執行(不會建立新執行緒!)。
範例:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("來自執行緒的 run(): " + Thread.currentThread().getName()));
thread.run(); // 在主執行緒中被呼叫!
thread.start(); // 在獨立的執行緒中被呼叫!
}
}
6. 實作:做一個簡單的多執行緒應用
讓我們繼續擴充教學用應用程式——假設我們要寫一個簡單的日誌系統,每個執行緒都寫出自己的訊息。
public class LoggerTask implements Runnable {
private final String message;
public LoggerTask(String message) {
this.message = message;
}
@Override
public void run() {
System.out.println("[" + Thread.currentThread().getName() + "] 日誌: " + message);
}
}
public class Main {
public static void main(String[] args) {
Thread logger1 = new Thread(new LoggerTask("系統啟動"));
Thread logger2 = new Thread(new LoggerTask("載入資料"));
Thread logger3 = new Thread(new LoggerTask("處理請求"));
logger1.start();
logger2.start();
logger3.start();
System.out.println("主執行緒結束工作.");
}
}
發生了什麼?
三個執行緒會並行地把各自的訊息寫到主控台。輸出順序不保證——這正是多執行緒的本質。
7. 有用的細節
表格:建立執行緒方法的比較
| 方式 | 彈性 | 可重用性 | 建議使用? |
|---|---|---|---|
| 繼承 Thread | 低 | 否 | 僅適用於簡單/教學範例 |
| 實作 Runnable | 高 | 是 | 是 |
| 匿名類別 | 中 | 否 | 適用於一次性任務 |
| Lambda 運算式 | 高 | 否 | 是(Java 8+) |
示意圖:執行緒如何被啟動
+----------------------+
| main (主執行緒) |
+----------------------+
|
v
+----------------------+
| Thread thread = ... |
+----------------------+
|
v
+----------------------+
| thread.start() | ← 執行緒「甦醒」
+----------------------+
|
v
+----------------------+
| 在新執行緒中執行 run() |
+----------------------+
|
v
+----------------------+
| 執行緒已結束 |
+----------------------+
8. 啟動執行緒時的常見錯誤
錯誤一:呼叫 run() 而不是 start()。
新手最常見的錯誤,就是直接在執行緒物件上呼叫 run()。這樣不會建立新執行緒,該方法只會在目前(主)執行緒中執行。正確作法:啟動執行緒一定要使用 start()。
錯誤二:重複啟動同一個執行緒。
同一個 Thread 物件不能呼叫 start() 超過一次——否則會拋出 IllegalThreadStateException。若需要再次啟動任務,請建立新的 Thread 物件。
錯誤三:未同步地修改共享變數。
若多個執行緒同時讀寫相同變數,結果可能出乎意料(競態條件)。同步機制之後會談,但目前——請小心。
錯誤四:沒有考慮等待執行緒結束。
若必須等待執行緒完成,請使用方法 join()。否則主要執行緒可能會比子執行緒更早結束。
錯誤五:混淆執行緒名稱與類別名稱.
方法 getName() 回傳的是執行緒名稱,而不是類別名稱。為了除錯,最好透過建構函式或 setName() 明確設定執行緒名稱。
GO TO FULL VERSION