多線程下載技術由來已久,早期的 FlashGet、NetAnts 等工具便善用 HTTP 協定的 Range 標頭,將檔案切分為多個區塊並行下載,最後合併成完整檔案。這種「分而治之」的思維,同樣適用於一般性的任務分派問題:給定一個待執行任務列表與指定線程數,如何將任務公平、高效地分配給各線程?
採用 連續分段 + 餘數前置 原則:
minTaskCount = 總任務數 / 線程數。remainTaskCount = 總任務數 % 線程數 依序分配給前幾個線程,每個多分配一筆。此策略保證任務在數量上均衡,且每線程處理的任務在邏輯上保持連續,便於追蹤與除錯。缺點是屬於靜態分配,快線程完成後無法協助慢線程(無工作盜演機制)。
以下程式碼整合了任務分發器 TaskDistributor、任務模型 Task 以及工作線程 WorkThread,採用命令模式封裝任務執行邏輯,若搭配監聽器(觀察者模式)即可即時反映進度,彷彿下載工具的區塊著色跳動。
package com.unmi.common;
import java.util.ArrayList;
import java.util.List;
/**
* 任務分發器:將任務列表按策略切分給多線程
* @author Unmi
*/
public class TaskDistributor {
/** 測試入口 */
public static void main(String[] args) {
// 準備 108 筆任務
List<Task> taskList = new ArrayList<>();
for (int i = 0; i < 108; i++) {
taskList.add(new Task(i));
}
int threadCount = 5; // 指定 5 條線程
List<Task>[] taskListPerThread = distributeTasks(taskList, threadCount);
System.out.println("實際啟動線程數:" + taskListPerThread.length);
for (int i = 0; i < taskListPerThread.length; i++) {
new WorkThread(taskListPerThread[i], i).start();
}
}
/**
* 核心分配演算法
* @param taskList 待分派任務列表
* @param threadCount 期望線程數
* @return 每個元素對應一線程的任務子列表
*/
public static List<Task>[] distributeTasks(List<Task> taskList, int threadCount) {
int minTaskCount = taskList.size() / threadCount;
int remainTaskCount = taskList.size() % threadCount;
// 僧多粥少時,實際線程數 = 任務數
int actualThreadCount = minTaskCount > 0 ? threadCount : remainTaskCount;
@SuppressWarnings("unchecked")
List<Task>[] taskListPerThread = new List[actualThreadCount];
int taskIndex = 0;
int remainIndices = remainTaskCount;
for (int i = 0; i < actualThreadCount; i++) {
taskListPerThread[i] = new ArrayList<>();
// 分配基礎任務
if (minTaskCount > 0) {
for (int j = taskIndex; j < minTaskCount + taskIndex; j++) {
taskListPerThread[i].add(taskList.get(j));
}
taskIndex += minTaskCount;
}
// 餘數前置分配
if (remainIndices > 0) {
taskListPerThread[i].add(taskList.get(taskIndex++));
remainIndices--;
}
}
// 列印分配結果
for (int i = 0; i < taskListPerThread.length; i++) {
List<Task> list = taskListPerThread[i];
System.out.printf("線程 %d 任務數:%d 區間[%d,%d]%n",
i, list.size(), list.get(0).getTaskId(), list.get(list.size() - 1).getTaskId());
}
return taskListPerThread;
}
}
/** 任務模型:支援狀態流轉 (READY → RUNNING → FINISHED) */
class Task {
public static final int READY = 0, RUNNING = 1, FINISHED = 2;
private int status = READY;
private final int taskId;
public Task(int taskId) { this.taskId = taskId; }
public void execute() {
setStatus(RUNNING);
System.out.println("當前線程:" + Thread.currentThread().getName()
+ " | 任務 ID:" + taskId);
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
setStatus(FINISHED);
}
public void setStatus(int status) { this.status = status; }
public int getTaskId() { return taskId; }
}
/** 自訂工作線程:持有專屬任務列表並依序執行 */
class WorkThread extends Thread {
private final List<Task> taskList;
private final int threadId;
public WorkThread(List<Task> taskList, int threadId) {
this.taskList = taskList;
this.threadId = threadId;
}
@Override
public void run() {
for (Task task : taskList) task.execute();
}
}
線程 0 任務數:22 區間[0,21] 線程 1 任務數:22 區間[22,43] 線程 2 任務數:22 區間[44,65] 線程 3 任務數:21 區間[66,86] 線程 4 任務數:21 區間[87,107] 實際啟動線程數:5 當前線程:Thread-0 | 任務 ID:0 當前線程:Thread-1 | 任務 ID:22 ...
ExecutorService 管理線程生命週期,避免頻繁建立/銷毀開銷。這個基礎實作雖簡單,卻是理解平行任務分派的良好起點。實務上可根據任務粒度、執行時間變異度選擇靜態或動態策略,甚至混合使用。