繼承 Thread 類是 Java 多線程編程的入門方式。本文通過一個「年份點擊日誌並發統計」的實戰案例,展示如何拆分任務、啟動線程、等待完成並聚合結果,同時指出代碼中存在的同步與效率問題。
需求:統計某一年每月的文章點擊量。日誌文件按天存儲(如 article.click.2016-05-20.txt),需解析全年 12 個月、每月 28-31 天的文件,統計行數作為點擊量。
public void yearlyClickThread() {
String year = getPara("year");
List<String> xList = Arrays.asList("January",..., "December");
List<Integer> yList = new ArrayList<>();
List<Thread> threadList = new ArrayList<>();
for (int m = 1; m <= 12; m++) {
// 1. 構建當月所有日期
List<String> dateList = buildDates(year, m);
// 2. 創建並啟動線程
Thread thread = new ReadLogFileThreadByYear(dateList);
thread.start();
// 3. 簡陋的節流:睡眠 1 秒
Thread.sleep(1000L);
threadList.add(thread);
}
// 4. 輪詢檢查線程狀態
int terminated = 0;
for (Thread t : threadList) {
if (t.getState() == Thread.State.TERMINATED) terminated++;
}
if (terminated == 12) { // 全部結束
// 5. 從靜態共享列表讀取結果
List<Map<String, Object>> list = ReadLogFileThreadByYear.list;
// 聚合邏輯...
renderJson();
}
}
public class ReadLogFileThreadByYear extends Thread {
private List<String> clickDate;
// 靜態共享列表 —— 所有線程並發寫入,無同步!
public static List<Map<String, Object>> list = new ArrayList<>();
public void run() {
Map<String, Object> map = new HashMap<>();
int monthClick = 0;
for (String date : clickDate) {
File file = new File(LOG_LOCATION, "article.click." + date + ".txt");
if (!file.exists()) continue;
// 逐行讀取計數...
monthClick += countLines(file);
}
map.put("month", dateList.get(0).substring(5,7));
map.put("clickCount", monthClick);
list.add(map); // 非線程安全操作
}
}
| 問題 | 風險 | 改進方案 |
|---|---|---|
靜態 List 並發 add 無同步 | 數據丟失、下標越界、內部數組擴容異常 | 用 Collections.synchronizedList、CopyOnWriteArrayList 或線程局部存儲最後匯總 |
Thread.sleep(1000) 節流啟動 | 人為延長總耗時,無法保證線程真正啟動完成 | 用 CountDownLatch/CyclicBarrier 或線程池 invokeAll |
輪詢 Thread.getState() 判斷完成 | 狀態可變、不可靠,主線程可能過早/過晚讀取結果 | thread.join() 阻塞等待,或 Future.get() |
繼承 Thread 而非實現 Runnable/Callable | 職責不清,難以復用、難以接入線程池 | 實現 Callable<Map>,配合 ExecutorService |
| IO 密集型任務無線程池限制 | 12 個月並發 12 線程尚可,若擴展到天級並發數百線程會耗盡資源 | 固定大小線程池 Executors.newFixedThreadPool(IO 核心數*2) |
| 異常吞沒 | catch (Exception e) { e.printStackTrace(); } 導致主線程感知不到失敗 | 拋出受檢異常或返回 Future 統一處理 |
public void yearlyClickThreadModern() throws Exception {
String year = getPara("year");
ExecutorService pool = Executors.newFixedThreadPool(8); // IO 密集型適當放大
List<Future<MonthResult>> futures = new ArrayList<>();
for (int m = 1; m <= 12; m++) {
final int month = m;
futures.add(pool.submit(() -> {
List<String> dates = buildDates(year, month);
int total = 0;
for (String d : dates) {
File f = new File(LOG_LOCATION, "article.click." + d + ".txt");
if (f.exists()) total += countLines(f);
}
return new MonthResult(month, total);
}));
}
List<Integer> yList = new ArrayList<>(Collections.nCopies(12, 0));
int totalCount = 0;
for (Future<MonthResult> f : futures) {
MonthResult r = f.get(); // 阻塞等待結果,自動傳播異常
yList.set(r.month - 1, r.count);
totalCount += r.count;
}
pool.shutdown();
renderJson(totalCount, yList);
}
record MonthResult(int month, int count) {}
Thread 適合教學演示,生產環境**首選 Runnable/Callable + 線程池**。Future/CompletionService,不要輪詢 Thread.getState()。CPU 核心數 * 2 或更大,配合非阻塞 IO (NIO/Netty) 效果更佳。從「能跑通」到「健壯高效」,多線程編程的核心是**任務分解、狀態隔離、同步協調**三板斧。