線程創建方式選擇指南:任務與線程分離的設計智慧
核心結論先行
實現 Runnable 介面 (方式二) 是優選,因為它實現了線程任務與線程對象的解耦,符合單一職責原則、開閉原則,支援線程池、避免繼承限制。
方式一:繼承 Thread 類
代碼示例:四個窗口賣票 (有缺陷版)
class Ticket extends Thread {
private static int num = 50; // static 共享
@Override
public void run() {
while (num > 0) {
System.out.println(Thread.currentThread().getName() + "...sale..." + num--);
}
}
}
public class Main {
public static void main(String[] args) {
new Ticket().start(); // 窗口1
new Ticket().start(); // 窗口2
new Ticket().start(); // 窗口3
new Ticket().start(); // 窗口4
}
}
缺陷分析
| 問題 | 說明 | 後果 |
| 任務與線程綁定 | Ticket 既是任務邏輯,又是線程控制 | 創建 4 個線程 = 創建 4 個 Ticket 實例 = 4 份邏輯副本 |
| 共享狀態靠 static | num 必須 static 才能共享 | 破壞封裝、污染類空間、繼承時易出錯 |
| 無法復用任務 | 想換賣票邏輯必須改類/新建類 | 違反開閉原則 |
| 單繼承限制 | 已繼承 Thread,無法繼承業務基類 | 設計受限 |
方式二:實現 Runnable 介面 (推薦)
代碼示例:同一任務對象,多線程執行
class Ticket implements Runnable {
private int num = 50; // 非 static,單一任務實例內部狀態
@Override
public void run() {
while (num > 0) {
System.out.println(Thread.currentThread().getName() + "...sale..." + num--);
}
}
}
public class Main {
public static void main(String[] args) {
Ticket task = new Ticket(); // 單一任務實例
new Thread(task, "窗口-A").start();
new Thread(task, "窗口-B").start();
new Thread(task, "窗口-C").start();
new Thread(task, "窗口-D").start();
}
}
優勢總結
| 優勢 | 說明 | 價值 |
| 任務線程分離 | Runnable 只管「做什麼」,Thread 只管「怎麼跑」 | 單一職責、高內聚 |
| 共享狀態自然 | 同一 task 實例的 num 自然共享,無需 static | 封裝性好、線程安全可控 |
| 支援線程池 | ExecutorService.execute(runnable) | 生產級併發控制基石 |
| 無繼承限制 | 可繼承業務基類、實現多介面 | 設計靈活 |
| Lambda 友好 | () -> { ... } 直接作為 Runnable | 代碼簡潔 |
第三種方式:實現 Callable + FutureTask (有返回值/拋異常)
Callable task = () -> {
int sum = 0;
for (int i = 0; i < 100; i++) sum += i;
return sum;
};
FutureTask futureTask = new FutureTask<>(task);
new Thread(futureTask).start();
Integer result = futureTask.get(); // 阻塞獲取結果
System.out.println("Sum: " + result);
生產環境終極選擇:線程池
// 固定大小線程池
ExecutorService executor = Executors.newFixedThreadPool(4);
// 提交 Runnable
executor.execute(() -> System.out.println("Task running"));
// 提交 Callable
Future future = executor.submit(() -> "Result");
// 關閉
executor.shutdown();
阿里巴巴開發手冊強制規定:線程資源必須通過線程池提供,不允許顯式 new Thread。
對比總結表
| 維度 | 繼承 Thread | 實現 Runnable | Callable + FutureTask | 線程池 |
| 任務復用 | 差 | 優 | 優 | 優 |
| 共享數據 | 靠 static | 自然 | 自然 | 自然 |
| 有返回值 | 麻煩 | 不可 | 支援 | 支援 |
| 拋受檢異常 | 不可 | 不可 | 支援 | 支援 |
| 線程控制 | 強 | 弱(委託Thread) | 弱 | 專業 |
| 繼承限制 | 有 | 無 | 無 | 無 |
| 推薦場景 | 教學/簡單測試 | 業務邏輯封裝 | 需結果/異常 | 所有生產代碼 |
避坑指南
- 不要為了共享數據而用 static:用同一 Runnable 實例
- 不要繼承 Thread 又想復用邏輯:改 Runnable
- 不要在 run() 中吞異常:用 Callable 或線程池的 UncaughtExceptionHandler
- 不要手動 new Thread:注入 ThreadPoolTaskExecutor / ExecutorService
設計哲學:「任務」是「做什麼」的邏輯,「線程」是「怎麼跑」的資源。 將兩者分離,是併發編程從入門到專業的分水嶺。Runnable + 線程池,是 Java 併發的標準範式。
- 上一篇文章: Spring 整合 JPA 完整指南:LocalEntityManagerFactoryBean 注入、JpaTransactionManager 事務管理、EntityManager 生命週期
- 下一篇文章: Map 遍歷的四種核心方式與效能對比
Copyright ©
程式碼之家 All Rights Reserved