首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 目錄大小計算六種實現方案性能大比拼:從遞歸到 ForkJoinPool 並行優化

     編輯:Java 相關知識

問題定義:遞歸遍歷目錄樹計算總文件大小

給定根目錄,遞歸累加所有常規文件大小。IO 密集型任務,順序遞歸受限於單線程磁盤調度,並行化可顯著提升吞吐(特別是 SSD/RAID/網絡存儲)。

方案一:單線程遞歸基準版

public class SequentialSize {
    public long calculate(File root) {
        if (root.isFile()) return root.length();
        long total = 0;
        File[] children = root.listFiles();
        if (children != null) {
            for (File child : children) {
                total += calculate(child);
            }
        }
        return total;
    }
}

特點:代碼最簡潔,棧深度受限於目錄層級,大目錄棧溢出風險,無法利用多核。

方案二:固定線程池 + Callable + Future 批次提交

public class FixedPoolBatch {
    private final ExecutorService pool = Executors.newFixedThreadPool(100);
    
    public long calculate(File root) throws InterruptedException, ExecutionException {
        long total = 0;
        List dirs = new ArrayList<>();
        dirs.add(root);
        
        while (!dirs.isEmpty()) {
            List> futures = new ArrayList<>();
            // 提交當前層級所有目錄任務
            for (File dir : dirs) {
                futures.add(pool.submit(() -> scanDir(dir)));
            }
            dirs.clear();
            // 收集結果,發現子目錄加入下輪
            for (Future f : futures) {
                DirResult r = (DirResult) f.get(); // 需自定義結果類攜帶子目錄列表
                total += r.size;
                dirs.addAll(r.subDirs);
            }
        }
        return total;
    }
    
    private DirResult scanDir(File dir) { /* 返回 size + subDirs */ }
}

缺陷:層級同步屏障,淺層目錄並行度低,深層目錄任務堆積,Future.get() 串行等待。

方案三:遞歸提交 Callable(任務爆炸風險)

public class NaiveRecursiveSubmit {
    private long calculate(ExecutorService pool, File file) throws Exception {
        if (file.isFile()) return file.length();
        List> futures = new ArrayList<>();
        for (File child : file.listFiles()) {
            futures.add(pool.submit(() -> calculate(pool, child))); // 遞歸提交
        }
        return futures.stream().mapToLong(f -> f.get()).sum();
    }
}

嚴重問題:文件數 = 任務數,百萬文件創建百萬任務,線程池隊列 OOM,上下文切換開銷巨大。

方案四:CountDownLatch + AtomicLong 計數器協調

public class LatchAtomic {
    private final ExecutorService pool = Executors.newFixedThreadPool(100);
    private final AtomicLong pending = new AtomicLong(1); // 根任務
    private final AtomicLong total = new AtomicLong();
    private final CountDownLatch latch = new CountDownLatch(1);
    
    public long calculate(File root) throws InterruptedException {
        pool.execute(() -> scan(root));
        latch.await(100, TimeUnit.SECONDS);
        return total.get();
    }
    
    private 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();
    }
}

優勢:無層級屏障,動態任務提交,原子計數器精確協調完成時刻。

方案五:BlockingQueue + 生產者消費者模式

public class QueueBased {
    private final ExecutorService pool = Executors.newFixedThreadPool(100);
    private final BlockingQueue sizeQueue = new ArrayBlockingQueue<>(500);
    private final AtomicLong pending = new AtomicLong();
    
    public long calculate(File root) throws InterruptedException {
        pending.incrementAndGet();
        pool.execute(() -> scan(root));
        
        long total = 0;
        while (pending.get() > 0 || !sizeQueue.isEmpty()) {
            Long size = sizeQueue.poll(10, TimeUnit.SECONDS);
            if (size != null) total += size;
        }
        return total;
    }
    
    private 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();
        }
        sizeQueue.offer(size); // 隔離結果收集
        pending.decrementAndGet();
    }
}

特點:結果通過隊列傳遞,解耦計算與聚合,隊列容量可背壓防 OOM。

方案六:JDK 7+ ForkJoinPool 分治遞歸(推薦)

public class ForkJoinSize {
    private static final ForkJoinPool POOL = new ForkJoinPool();
    
    public long calculate(File root) {
        return POOL.invoke(new SizeTask(root));
    }
    
    private static class SizeTask extends RecursiveTask {
        private final File file;
        SizeTask(File file) { this.file = file; }
        
        @Override
        protected Long compute() {
            if (file.isFile()) return file.length();
            
            long size = 0;
            List tasks = new ArrayList<>();
            for (File child : file.listFiles()) {
                if (child.isFile()) {
                    size += child.length();
                } else {
                    tasks.add(new SizeTask(child));
                }
            }
            // 批量 fork 子任務
            invokeAll(tasks);
            // join 收集結果
            for (SizeTask t : tasks) size += t.join();
            return size;
        }
    }
}

核心優勢:

  • 工作竊取算法:空閒線程竊取繁忙線程隊列任務,負載均衡
  • 分治遞歸天然適配目錄樹結構
  • 無顯式同步原語,代碼簡潔
  • JVM 內部優化,任務調度開銷極低

性能對比預期(SSD、萬級文件目錄)

生產環境優化建議

  1. NIO.2 Files.walkFileTree:替代 File.listFiles(),支持流式處理、屬性預取、符號鏈接控制
  2. 並行流 (JDK 8+):Files.walk(root).parallel().mapToLong(File::length).sum() 簡潔但不可控並行度
  3. 直接 IO / mmap:極大文件場景繞過頁面緩存
  4. 監控指標:任務隊列長度、線程活躍度、GC 頻率、磁盤 IOPS/吞吐
  5. 錯誤處理:權限不足、文件被刪除、網絡存儲超時,需 try-catch 繼續遍歷
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
方案預期耗時CPU 利用率代碼複雜度推薦指數
單線程遞歸基準 1.0x~12%⭐⭐⭐
FixedPool 批次0.6x~45%⭐⭐⭐⭐⭐
CountDownLatch0.4x~70%⭐⭐⭐⭐⭐⭐⭐
BlockingQueue0.4x~70%⭐⭐⭐⭐⭐⭐⭐
ForkJoinPool0.35x~85%⭐⭐⭐⭐⭐⭐⭐