計算資料夾總大小屬於典型 IO 密集型遞歸任務。順序遞歸無法充分利用多核 CPU 與現代存儲(SSD/NVMe/RAID)並行吞吐能力。本文系統對比六種並行化方案,從線程池到 ForkJoin 分治,提供生產級選型指南。
| 方案 | 並行模型 | 同步機制 | 適用場景 | 複雜度 |
|---|---|---|---|---|
| 1. 單線程遞歸 | 無 | 調用棧 | 小目錄、教學、嵌入式 | 低 |
| 2. 固定線程池+批次 | 層級並行 | Future.get() 屏障 | 淺層寬目錄樹 | 中 |
| 3. 遞歸提交 Callable | 任務級並行 | Future.get() 串行 | ❌ 任務爆炸風險 | 中 |
| 4. CountDownLatch+AtomicLong | 動態任務提交 | 原子計數器+門闩 | 深層目錄、通用場景 | 中高 |
| 5. BlockingQueue 生產消費 | 動態任務提交 | 隊列緩衝+原子計數 | 需背壓控制、結果流式處理 | 中高 |
| 6. ForkJoinPool 分治 | 工作竊取分治 | 框架內部調度 | ✅ 推薦:通用最優 | 低 |
private final AtomicLong pending = new AtomicLong(1); // 根任務計數
private final CountDownLatch latch = new CountDownLatch(1);
void scan(File file) {
long size = 0;
if (file.isDirectory()) {
for (File child : file.listFiles()) {
if (child.isDirectory()) {
pending.incrementAndGet(); // 新任務
pool.execute(() -> scan(child));
} else {
size += child.length();
}
}
} else {
size = file.length();
}
total.addAndGet(size);
if (pending.decrementAndGet() == 0) latch.countDown(); // 最後任務觸發
}
優勢:無層級同步等待,任務動態展開,AtomicLong 無鎖計數極高效。
private final BlockingQueue sizeQueue = new ArrayBlockingQueue<>(500);
// ...
sizeQueue.offer(size); // 非阻塞/超時放入
// 聚合線程
Long size = sizeQueue.poll(10, TimeUnit.SECONDS); // 超時獲取
優勢:結果傳輸異步化,隊列容量天然背壓防 OOM,聚合邏輯可獨立擴展(如寫入數據庫、發送消息)。
private static class SizeTask extends RecursiveTask {
private final File file;
@Override
protected Long compute() {
if (file.isFile()) return file.length();
long size = 0;
List forks = new ArrayList<>();
for (File child : file.listFiles()) {
if (child.isFile()) size += child.length();
else forks.add(new SizeTask(child));
}
invokeAll(forks); // 批量 fork
for (SizeTask t : forks) size += t.join(); // join 收集
return size;
}
}
核心機制:
方案 1: 12.4s (基準) 方案 2: 7.1s (1.7x) 方案 3: OOM (任務過多) 方案 4: 4.8s (2.6x) 方案 5: 4.9s (2.5x) 方案 6: 4.2s (3.0x) ⭐ 最優
public class ProductionDirSize {
private static final ForkJoinPool POOL = new ForkJoinPool(
Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
(t, e) -> log.error("ForkJoin worker error", e), // 異常處理
true // 異步模式
);
public static long calculate(Path root) throws IOException {
return POOL.invoke(new SizeTask(root));
}
private static class SizeTask extends RecursiveTask {
private final Path path;
SizeTask(Path path) { this.path = path; }
@Override
protected Long compute() {
try {
if (Files.isRegularFile(path)) {
return Files.size(path);
}
long size = 0;
List forks = new ArrayList<>();
try (Stream children = Files.list(path)) { // 流式列舉,自動關閉
for (Path child : (Iterable) children::iterator) {
if (Files.isDirectory(child)) {
forks.add(new SizeTask(child));
} else {
size += Files.size(child);
}
}
}
invokeAll(forks);
for (SizeTask t : forks) size += t.join();
return size;
} catch (IOException e) {
log.warn("Access denied or IO error: {}", path, e);
return 0L; // 忽略不可訪問文件
}
}
}
}
Executors.newFixedThreadPool 提交百萬任務 → OOM(用 ForkJoin 或有界隊列)File.listFiles() 返回 null(權限不足、非目錄)Files.walkFileTree 可配置 FileVisitOption.FOLLOW_LINKSFiles.size() 替代 file.length(),拋出具體異常便於處理