怎樣將大批量文件進行循環分組(reduce)?

背景

   當有時候一個文件夾下有幾萬個幾十萬個文件時,我們的桌面終端打開這個文件夾可能會卡。或者將文件進行批量上傳時,如果是在文件夾下全選,那麼基本上瀏覽器就卡死了,當然也不能這樣子操作滴~

   題主最近就遇到這樣一個問題,批量上傳文件,有幾萬個,擔心全選會搞崩瀏覽器或者cmd終端,於是打算將數據分組,分批次上傳,減少風險壓力。可能有的同學會說,那簡單嘛,直接ctrl C+V完事兒,但是人這個眼睛吶,越集中注意力看一個字,就越不覺得它像個字,所以難免會出錯的,而且拖動也會很卡。

作為一個搞電腦的工程師(程序猿),能用電腦解決的,怎麼能浪費體力呢[滑稽]

參考步驟

   其實要實現這麼一個東西,很簡單的,二話不多說,直接一個for循環搞定 歐耶!但是呀,那個速度呀,難以忍受,如果分組這個文件還需要去做一些額外的操作,那豈不是更慢,說到這,想到以前讀書學習大數據的時候,分詞計算map-reduce那每次一跑就是一個小時過去了,所以,光寫出來不得行,還得優化。而且分組的時候有一些注意點要注意。

   題主的大致步驟如下:

  1. 打開文件夾,遍歷文件;
  2. 啟用線程池,多線程跑批任務,加快速度;
  3. 計數文件夾中文件個數,達到指定數量,建立下一個文件夾;
  4. 使用新的文件夾繼續移動或複製文件,操作過程中進行重命名;重複3,4步驟
  5. 操作完畢,等待線程池任務處理完畢,銷毀。

代碼實現

   說了那麼多,還是直接上代碼吧:)


import java.io.File;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.*;

public class TestReduceFile {

    /**
     * 文件計數
     */
    private static volatile int currentFileNum = 0;

    private static final String initFilePathName = "/Users/anhaoo/test/reduceFile/reduce";

    private static volatile String currentFilePathName;

    public static void main(String[] args) {
        groupFile("/User/anhaoo/test/big_pdf");
    }

    /**
     * 將文件分組:分成一個文件夾多少個文件這種
     * @param oldFilePath
     */
    private static void groupFile(String oldFilePath) {
        File file = new File(oldFilePath);
        File[] fileList = file.listFiles();
        // 線程池批處理:鏈表阻塞隊列
        ExecutorService executor = Executors.newFixedThreadPool(80);
        if (Objects.nonNull(fileList) && fileList.length > 0) {
            // System.out.println(fileList.length);
            Arrays.stream(fileList).forEach(item -> {
                // 線程池提交任務處理
                if (!item.isDirectory()) {
                    executor.execute(() -> groupFileSub(item));
                }
            });
        }
        // shutdown 等待任務全部執行完畢 銷毀
        executor.shutdown();
    }


    /**
     * 分組文件,每滿n個文件生成下一個文件夾,然後將後續遍歷的文件移動到下一個文件夾中
     * @param oldFile
     */
    private static synchronized void groupFileSub(File oldFile) {
        // 判斷是否需要新生成文件夾,一個文件夾下放700個
        if (currentFileNum % 700 == 0) {
            // reduce0,reduce1,reduce2......
            String newFileFolder = initFilePathName.concat(String.valueOf(currentFileNum/700));
            // 將新的文件夾名賦值給共享變量
            currentFilePathName = newFileFolder;
        }
        File aimFilePath = new File(currentFilePathName);
        if (!aimFilePath.exists()) {
            aimFilePath.mkdirs();
        }
        try {
            // 移動文件時順便重命名文件
            String oldFileName = oldFile.getName();
            String[] fileNameArr = oldFileName.split("_");
            // 加下面這個if的原因 是因為mac電腦下有一個隱藏的.DS_Store文件,它按我的規則重命名時會影響我的操作
            // 所以判斷一下,不然我這裡會拋越界異常:)
            if (fileNameArr.length < 3) {
                return;
            }
            String uuid = fileNameArr[2];
            String newFileName = "/pdfFile_".concat(uuid);
            File aimFile = new File(aimFilePath + File.separator + newFileName);
            currentFileNum++;
            oldFile.renameTo(aimFile);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

   代碼就如上了,效果如圖所示:

701 是因為有一個隱藏文件了哈!

注意事項

   有幾個點需要注意一下:上面程序中使用了volatile和synchronized,以及線程池等工具,

  • 使用線程池,是為了多個線程一起處理,加快效率;
  • 使用volatile修飾了兩個變量,因為currentFileNum會實時變化,currentFilePathName文件夾也會在執行過程中發生變化

   可能有的同學會有疑惑,既然使用了volatile關鍵保證了多線程變量的可見性,那為什麼還要使用synchronize開持鎖同步呢?哈哈哈,剛開始我也是這麼認為的,沒有使用鎖,直接跑,但是每次跑完後每個文件夾的數量不僅不相等而且數量還不一致,不止700個多,後來仔細一想,volatile雖然保持了變量的可見性,但是當多個線程拿到這個變量是將變量副本拷貝到自己的棧內存中,只能保證在獲取變量的時候是最新的,但是CPU指令的執行是哪個線程搶到了就去執行,所以可能剛好那個時候其他線程又將變量修改了,因為計數變量currentFileNum在不停地自增,導致線程不安全,不符合我們的預期效果,這個也再一次證明了一個結論:

volatile只是保證了可見性,但是線程變量的安全它無法保證;

所以在這個方法上加了一個鎖,保證線程的安全性;

   上述就是本文想分享的東西了,如果有更好的方法或者文章中有不足之處歡迎大家指出,共同進步才是我們的宗旨!