CodeGym /課程 /JAVA 25 SELF /ExecutorService、Callable、Future:啟動任務

ExecutorService、Callable、Future:啟動任務

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

1. ExecutorService:像專業人士一樣管理執行緒

為什麼不該只是用 new Thread 直接建立執行緒

剛開始學多執行緒時,一切看起來都很簡單:

Thread t = new Thread(() -> {
    // 做點事
});
t.start();

這樣的方式可行,但當任務變多時很快就成為負擔。每次呼叫 new Thread() 都會建立新的執行緒,而數十或數百個執行緒會讓系統過載。而且管理起來不方便:要追蹤它們何時結束、發生錯誤該怎麼辦、如何停止並重複利用,等等。

這時就輪到 ExecutorService — 智慧的執行緒調度器登場。你只需把任務交給它,它會自行決定由哪個執行緒、何時來執行。結果通常更快、更穩定,也更省心。

ExecutorService 的運作方式

ExecutorService 的原理簡單但有效。

  • 內部有一個執行緒池 — 預先建立的一組工作執行緒(固定或動態)。
  • 任務會進入佇列,並由空閒的執行緒取走執行。
  • 服務會管理生命週期:你可以等待完成、正常地關閉執行緒池並釋放資源。

建立 ExecutorService

最常見的方式 — 使用 Executors 類別的工廠方法:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService executor = Executors.newFixedThreadPool(4); // 4 個執行緒
  • newFixedThreadPool(N) — 含 N 個執行緒的池(適用於多數情境)。
  • newCachedThreadPool() — 動態池,按需建立執行緒(小心:任務暴增時可能耗盡記憶體)。
  • newSingleThreadExecutor() — 單一執行緒(順序執行)。

範例:透過 Runnable 在 ExecutorService 中執行

executor.submit(() -> {
    System.out.println("來自執行緒池的問候!");
});

當你完成對 ExecutorService 的使用後,必須正確地關閉它:

executor.shutdown(); // 禁止新增任務,並等待目前的任務完成

重要:如果不呼叫 shutdown(),程式可能不會結束 — 池中的執行緒會等待新任務。

2. Runnable vs Callable:任務有不同的型態

在 Java 5 之前,若你想在執行緒中做點事,會撰寫 Runnable 的實作。這種任務不會回傳任何結果,也不會拋出受檢例外。

Runnable task = () -> {
    System.out.println("只是執行,不會回傳任何東西!");
};
executor.submit(task);

Callable:有結果的任務(並可拋出例外)

有時我們希望任務不只「做點事」,還能回傳結果——例如數字總和、計算結果,或從伺服器取得的資料。這就是介面 Callable<T> 的用途。

import java.util.concurrent.Callable;

Callable<Integer> sumTask = () -> {
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
};
  • call() 方法會回傳型別為 T 的結果。
  • call() 方法可以拋出受檢例外。

類比:Runnable — 「去把碗洗好」(結果不重要),Callable — 「去端杯茶回來,並說說它的溫度」(結果很重要)。

啟動 Callable:若要取得結果,請使用 executor.submit(...)。它會回傳 Future<T> 物件。

3. Future:對結果的承諾

Future 就是「對未來會回傳結果的承諾」。當你把任務交給 ExecutorService 時,你會得到一個 Future,之後可以從中取得結果、查詢任務是否完成,或取消任務。

Future 的主要方法

  • T get() — 取得結果(會等待任務完成)。
  • boolean isDone() — 任務是否已完成。
  • boolean cancel(boolean mayInterruptIfRunning) — 嘗試取消任務。
  • boolean isCancelled() — 任務是否已被取消。

範例:啟動 Callable 並取得結果

import java.util.concurrent.*;

public class ParallelSumApp {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Callable<Integer> sumTask = () -> {
            int sum = 0;
            for (int i = 1; i <= 100; i++) sum += i;
            return sum;
        };

        Future<Integer> future = executor.submit(sumTask);

        System.out.println("任務已啟動,可以先去做其他事情...");

        // 取得結果(如果任務尚未完成,該方法會阻塞執行緒)
        Integer result = future.get();
        System.out.println("計算結果:" + result);

        executor.shutdown();
    }
}
  • 任務被送入執行緒池。
  • 在任務執行期間,主執行緒可以先去做其他事。
  • 當需要結果時,呼叫 future.get() —— 若任務尚在執行,呼叫端會等待。
  • 任務一完成,就能取得結果。

4. 實作:多個任務與等待完成

我們常需要一次啟動多個任務並等待全部完成。例如處理一個資料陣列,把它切分成多個部分,分別在各自的任務中計算每一部分的總和。

範例:分塊計算陣列元素的總和

import java.util.*;
import java.util.concurrent.*;

public class ParallelArraySum {
    public static void main(String[] args) throws Exception {
        int[] array = new int[1000];
        Arrays.setAll(array, i -> i + 1); // 填入 1 到 1000 的數字

        ExecutorService executor = Executors.newFixedThreadPool(4);

        int chunkSize = array.length / 4;
        List<Future<Integer>> futures = new ArrayList<>();

        for (int i = 0; i < 4; i++) {
            int from = i * chunkSize;
            int to = (i == 3) ? array.length : (i + 1) * chunkSize;

            Callable<Integer> sumTask = () -> {
                int sum = 0;
                for (int j = from; j < to; j++) sum += array[j];
                System.out.println("從 " + from + " 到 " + (to - 1) + " 的總和 = " + sum);
                return sum;
            };

            futures.add(executor.submit(sumTask));
        }

        int totalSum = 0;
        for (Future<Integer> f : futures) {
            totalSum += f.get(); // 逐一等待每個任務
        }

        System.out.println("總計:" + totalSum);

        executor.shutdown();
    }
}

這裡把陣列切成 4 個部分。對每一部分建立任務(Callable)來計算其總和。所有任務送交 ExecutorService,並取得 Future。最後收集每個任務的結果並相加。

在實務情境中,使用 invokeAll 一次等待所有任務完成會更方便。

5. 使用 Future 時的錯誤處理

當你呼叫 future.get() 時,若任務以例外結束,例外會被包裝成 ExecutionException。這點很重要:如果任務出了問題,你往往會在呼叫 get() 時才得知。

範例:處理例外

Callable<Integer> errorTask = () -> {
    throw new IllegalArgumentException("出了點問題!");
};

Future<Integer> badFuture = executor.submit(errorTask);

try {
    badFuture.get();
} catch (ExecutionException e) {
    System.out.println("任務以錯誤結束:" + e.getCause());
}
  • 在任務內拋出例外。
  • 呼叫 get() 時,例外會被「包裝」在 ExecutionException 中。
  • 可透過 getCause() 取得真正的原因。

6. 實用細節

如何取消任務

Future<?> f = executor.submit(() -> {
    while (true) {
        // 無窮迴圈工作
        if (Thread.currentThread().isInterrupted()) {
            System.out.println("有人請我結束了!");
            break;
        }
    }
});

Thread.sleep(100); // 稍微等一下
f.cancel(true); // 試著取消任務
  • cancel(true) 會嘗試中斷尚未完成的任務。
  • 在任務內建議檢查 Thread.currentThread().isInterrupted() 並妥善結束。

shutdown vs shutdownNow

shutdown() — 溫和停止:禁止加入新任務,讓目前的任務自行完成。最常用。

shutdownNow() — 強制停止:嘗試中斷正在執行的執行緒,並回傳尚未開始的任務清單。請謹慎使用。

invokeAll 與 invokeAny

invokeAll(Collection<Callable<T>> tasks) 會啟動所有傳入的任務並等待全部完成。回傳 Future 的清單。

invokeAny(Collection<Callable<T>> tasks) 只會等待第一個成功完成的任務,回傳其結果並取消其餘任務。當你只在意第一個成功回應時相當實用。

7. 使用 ExecutorService、Callable 與 Future 的常見錯誤

錯誤 1:沒有關閉 ExecutorService。 如果忘了呼叫 shutdown(),程式可能會在 main 結束後仍然「掛著」,因為執行緒池在等待新任務。

錯誤 2:在送出任務後立刻等待結果。 如果在 submit() 之後立即呼叫 get(),就無法享受非同步的好處——呼叫端仍會被阻塞。請在等待結果前,並行地做其他有用的工作,只在真正需要時才取得結果。

錯誤 3:忽略任務中的例外。 如果在呼叫 get() 時不處理 ExecutionException,可能會錯過任務中發生的重要錯誤。

錯誤 4:在多個任務間未同步地共享可變狀態。 若多個任務操作同一份資料——就需要同步機制或使用具備執行緒安全的集合。

錯誤 5:建立過多的執行緒。 不要讓執行緒池的大小遠大於處理器核心數——這反而可能讓執行變慢。

錯誤 6:忘了取消不再需要的任務。 如果任務已經不需要,請透過 cancel() 取消它,以免浪費資源。

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