CodeGym /課程 /JAVA 25 SELF /啟動執行緒:Thread 與 Runnable,語法

啟動執行緒:Thread 與 Runnable,語法

JAVA 25 SELF
等級 51 , 課堂 1
開放

1. Thread 類別:在 Java 中的第一個執行緒

在 Java 中,每個執行緒都由類別 Thread 的物件表示。這個類別就像管弦樂的指揮——它負責控制執行緒的啟動、停止與生命週期。

要啟動一個執行緒,需要:

  1. 建立一個 Thread 類別(或其子類別)的物件。
  2. 在該物件上呼叫方法 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() 明確設定執行緒名稱。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION