Java多線程遍歷文件夾,廣度遍歷加多線程加深度遍歷結合


復習IO操作,突然想寫一個小工具,統計一下電腦里面的Java代碼量還有注釋率,最開始隨手寫了一個遞歸算法,遍歷文件夾,比較簡單,而且代碼層次清晰,相對易於理解,代碼如下:(完整代碼貼在最后面,前面是功能實現代碼)


   
   
  
  
          
  1. public static void visitFile(File file) {
  2. if (file != null) {
  3. // 如果是文件夾
  4. if (file.isDirectory()) {
  5. // 統計文件夾下面的所有文件路徑
  6. File[] fls = file.listFiles();
  7. // 如果父文件夾有內容
  8. if (fls != null) {
  9. // 那么遍歷子文件
  10. for ( int i = 0; i < fls.length; i++) {
  11. // 繼續判斷文件是文件夾還是文件,嵌套循環
  12. visitFile(fls[i]);
  13. }
  14. }
  15. } else // 如果是文件
  16. {
  17. // 判斷文件名是不是.java類型
  18. String fname = file.getName();
  19. if (fname.endsWith( ".java")) {
  20. Sysotem.out.println( "java文件:"+fname);
  21. }
  22. }
  23. }
  24. }


但是寫成小工具后,在使用中我發現了它遍歷速度還是比較慢的問題,遞歸算法本身運行效率低,占用空間也非常大,每一次調用都要出現方法壓棧彈棧,系統開銷大。所以我想把它改成非遞歸算法,我有兩個想法:1.打開父文件夾(父親)之后,遍歷子文件夾(兒子),如果是目錄就列出子文件夾的子文件夾(兒子的兒子),記錄下來,但是不繼續打開;如果遇到的是我需要的文件,那么就加入文件集合中,重復。代碼如下:


   
   
  
  
          
  1. File fl = this.file; //根文件(父親)
  2. ArrayList<File> flist = new ArrayList<File>(); //文件夾目錄列表1
  3. ArrayList<File> flist2 = new ArrayList<File>(); //文件夾目錄列表2
  4. ArrayList<File> tmp = null, next = null; //集合應用變量,tmp記錄子文件夾的目錄列表(兒子),next記錄子文件夾的子文件夾列表(兒子的兒子)
  5. flist.add(fl); //列表1記錄根文件
  6. // 廣度遍歷層數控制
  7. int loop = 0; //控制循環層數
  8. while (loop++ < 3) { // 此處只循環了三層
  9. tmp = tmp == flist ? flist2 : flist; //此處比較繞,實現功能是tmp和next兩個引用變量互換地址
  10. next = next == flist2 ? flist : flist2;
  11. for ( int i = 0; i < tmp.size(); i++) { //遍歷子文件夾
  12. fl = tmp.get(i);
  13. if (fl != null) {
  14. if (fl.isDirectory()) { //如果遇到目錄
  15. File[] fls = fl.listFiles();
  16. if (fls != null) {
  17. next.addAll(Arrays.asList(fls)); //將子文件夾的子文件夾目錄列表一次全部加入next列表
  18. }
  19. } else {
  20. if (fl.getName().endsWith(type)) {
  21. papList.add(fl); //如果是需要的文件,就加入papList列表
  22. }
  23. }
  24. }
  25. }
  26. tmp.clear(); //清空子文件夾列表,因為已經遍歷子文件夾結束,后面需要一個空的列表繼續裝東西
  27. }



  

2.第二種思路是打開父文件夾后,遍歷子文件夾,然后遇到目錄就繼續打開,直到沒有目錄才返回上一層,這個思路和遞歸遍歷算法一樣,看遞歸的算法更好理解,代碼如下:


   
   
  
  
          
  1. // 非遞歸深度遍歷算法
  2. void quickFind() throws IOException {
  3. // 使用棧,進行深度遍歷
  4. Stack<java.io.File> stk = new Stack<File>();
  5. stk.push( this.file); //父文件壓棧
  6. File f;
  7. while (!stk.empty()) { //當棧不為空,就一直循環壓棧出棧過程。
  8. f = stk.pop(); //彈出棧頂元素
  9. if (f.isDirectory()) { //如果棧頂是目錄
  10. File[] fs = f.listFiles(); //打開棧頂子目錄
  11. if (fs != null)
  12. for ( int i = 0; i < fs.length; i++) {
  13. stk.push(fs[i]); //將棧頂子目錄依次壓棧
  14. }
  15. } else {
  16. if (f.getName().endsWith(type)) {
  17. // 記錄所需文件的信息,加入集合
  18. papList.add(f);
  19. }
  20. }
  21. }
  22. }


上面的兩種非遞歸算法,速度上相差無幾,相對於遞歸算法比較難於理解,但是速度真的快一點,而且占用內存比較小,如果使用遞歸,當遞歸層數比較多的時候對系統資源消耗巨大,甚至會造成jvm崩潰,非遞歸算法沒有這個隱患,深度遍歷和廣度遍歷在遍歷很多文件時,深度遍歷稍微占優勢,速度會快一點,但是數據有浮動。后面我又復習到多線程,我就想把多線程加進去,會不會更快。然后我就建立了一個線程池:


   
   
  
  
          
  1. // 創建線程池,一共THREAD_COUNT個線程可以使用
  2. ExecutorService pool = Executors.newFixedThreadPool(THREAD_COUNT); //新建固定線程數的線程池
  3. for (File file : next) {
  4. pool.submit( new FileThread(file, type)); //提交對象到線程池,FileThread類是我自定義的內部類,重寫了Runnable接口中的run方法。
  5. }
  6. pool.shutdown(); //結束
  7. // 必須等到所有線程結束才可以讓主線程退出,不然就一直阻塞
  8. while (!pool.isTerminated())
  9. ;

線程池的好處是統一管理線程,不用一直開辟新的線程,開辟線程很消耗系統資源,線程池里面的線程可以循環使用,程序結束了再釋放,適用於頻繁切換任務的情況,在Tcp/ip網絡編程中常見。加入多線程我也有兩種想法,1.我先想到多線程就是幾個兄弟一起干活,速度肯定快,所以我每遍歷一個文件夾就開辟一個新的線程,代碼如下:


   
   
  
  
          
  1. void judge(File f) {
  2. if (f != null) {
  3. if (f.isDirectory()) {
  4. // 如果是目錄
  5. File[] fs = f.listFiles();
  6. if (fs != null)
  7. FileOP.BigFileList.addAll(Arrays.asList(fs));
  8. // 一起加到BigFileList中,前面有一個for循環遍歷BigFileList,遍歷一次開辟一個新線程
  9. } else {
  10. if (f.getName().endsWith(type)) {
  11. FileOP.papList.add(f);
  12. // 我們要的文件記錄下來
  13. }
  14. }
  15. }
  16. }
但是想法很美好,現實很殘酷,這種方法速度比遞歸算法還要慢,開辟新線程(此處還沒有應用線程池,每次都new Thread();)的時間,加上垃圾回收的時間遠超過遞歸算法遍歷文件夾的時間。而且多線程也並不是可以無限個,一般來說CPU大多只支持四線程,但是線程數大於四時,cpu通過調度算法分配程序執行的時間,常見先進先出,短作業優先,時間片輪轉調度,高優先權調度算法,我一般設置最大線程數是CPU支持線程數的3倍,根據我實際測試,設置成100個線程比設置成12個線程,程序執行時間沒有短多少,反而在CPU占用率高的時候100線程更慢。

    所以我又想,怎么才能發揮多線程的優勢呢,首先肯定要把一個任務分成多個任務,這也有兩個思路:1.先用遞歸深度遍歷算法遍歷文件夾,當遇到比較大的文件夾,比如說包含1000個子文件夾就記錄下來,然后跳過繼續遍歷其他的文件夾,此時主線程有一個while循環一直在檢查有沒有新的大文件夾出現,如果有就開一個新線程去遍歷大文件夾,代碼如下:


   
   
  
  
          
  1. void findAll(File f) {
  2. if (f != null) {
  3. if (f.isDirectory()) {
  4. // 如果是目錄
  5. File[] fs = f.listFiles();
  6. if (fs == null) {
  7. return;
  8. }
  9. * if (fs.length > FileOP.THREAD_COUNT * 100) { //
  10. * 當文件夾的目錄數量大於線程數的百倍,記錄下來,待會用多線程慢慢數 FileOP.BigFileList.add(f);
  11. * // 這記錄的都是后面要用多線程來數一數的 } else
  12. {
  13. for ( int i = 0; i < fs.length; i++) {
  14. findAll(fs[i]);
  15. // 如果文件數少,就遞歸一下
  16. }
  17. }
  18. } else {
  19. // 需要的文件放進pap集合
  20. if (f.getName().endsWith(type)) {
  21. FileOP.papList.add(f);
  22. }
  23. }
  24. }
  25. }
實際效果比不上單純的遞歸算法速度快,難受,因為我記錄的文件夾雖然是“大文件夾”,但是可能並不深,遞歸一兩層就結束了,這時候開新線程消耗更大,所以我就想到自上而下的分配任務,比如說我們讓程序遍歷C 盤所有的Java文件,程序可以先獲取C盤根目錄列表,然后開辟線程池,每一個線程執行一個子目錄的遍歷,遍歷子文件夾時換成非遞歸深度遍歷算法,算法如下:


   
   
  
  
          
  1. package com.ycs;
  2. import java.io.File;
  3. import java.io.FileInputStream;
  4. import java.io.IOException;
  5. import java.io.InputStream;
  6. import java.math.BigDecimal;
  7. import java.util.ArrayList;
  8. import java.util.Arrays;
  9. import java.util.Stack;
  10. import java.util.concurrent.ExecutorService;
  11. import java.util.concurrent.Executors;
  12. public class FileList {
  13. // 控制線程數,最優選擇是處理器線程數*3,本機處理器是4線程
  14. private final static int THREAD_COUNT = 12;
  15. // 線程共享數據,保存所有的type文件
  16. private ArrayList<File> papList = new ArrayList<File>();
  17. // 保存文件附加信息
  18. private ArrayList<String> contenList = new ArrayList<String>();
  19. // 當前文件或者目錄
  20. private File file;
  21. // 所需的文件類型
  22. private String type;
  23. public FileList() {
  24. super();
  25. // TODO Auto-generated constructor stub
  26. }
  27. public FileList(String f, String type) {
  28. super();
  29. this.file = new File(f);
  30. this.type = type;
  31. }
  32. public ArrayList<String> getContenList() {
  33. return contenList;
  34. }
  35. // 內部類繼承runnable接口,實現多線程
  36. class FileThread implements Runnable {
  37. private File file;
  38. private String type;
  39. public FileThread(File file, String type) {
  40. super();
  41. this.file = file;
  42. this.type = type;
  43. }
  44. public FileThread() {
  45. super();
  46. // TODO Auto-generated cosnstructor stub
  47. }
  48. @Override
  49. public void run() {
  50. try {
  51. quickFind();
  52. } catch (IOException e) {
  53. // TODO Auto-generated catch block
  54. e.printStackTrace();
  55. }
  56. }
  57. // 非遞歸深度遍歷算法
  58. void quickFind() throws IOException {
  59. // 使用棧,進行深度遍歷
  60. Stack<java.io.File> stk = new Stack<File>();
  61. stk.push( this.file);
  62. File f;
  63. while (!stk.empty()) {
  64. f = stk.pop();
  65. if (f.isDirectory()) {
  66. File[] fs = f.listFiles();
  67. if (fs != null)
  68. for ( int i = 0; i < fs.length; i++) {
  69. stk.push(fs[i]);
  70. }
  71. } else {
  72. if (f.getName().endsWith(type)) {
  73. // 記錄所需文件的信息
  74. papList.add(f);
  75. }
  76. }
  77. }
  78. }
  79. }
  80. public ArrayList<File> getPapList() {
  81. // 外部接口,傳遞遍歷結果
  82. return papList;
  83. }
  84. // 深度遍歷算法加調用線程池
  85. void File() {
  86. File fl = this.file;
  87. ArrayList<File> flist = new ArrayList<File>();
  88. ArrayList<File> flist2 = new ArrayList<File>();
  89. ArrayList<File> tmp = null, next = null;
  90. flist.add(fl);
  91. // 廣度遍歷層數控制
  92. int loop = 0;
  93. while (loop++ < 3) { // 最優循環層數是3層,多次實驗得出
  94. tmp = tmp == flist ? flist2 : flist;
  95. next = next == flist2 ? flist : flist2;
  96. for ( int i = 0; i < tmp.size(); i++) {
  97. fl = tmp.get(i);
  98. if (fl != null) {
  99. if (fl.isDirectory()) {
  100. File[] fls = fl.listFiles();
  101. if (fls != null) {
  102. next.addAll(Arrays.asList(fls));
  103. }
  104. } else {
  105. if (fl.getName().endsWith(type)) {
  106. papList.add(fl);
  107. }
  108. }
  109. }
  110. }
  111. tmp.clear();
  112. }
  113. // 創建線程池,一共THREAD_COUNT個線程可以使用
  114. ExecutorService pool = Executors.newFixedThreadPool(THREAD_COUNT);
  115. for (File file : next) {
  116. pool.submit( new FileThread(file, type));
  117. }
  118. pool.shutdown();
  119. // 必須等到所有線程結束才可以讓主線程退出,不然就一直阻塞
  120. while (!pool.isTerminated())
  121. ;
  122. }
  123. void info(File file) throws IOException {
  124. InputStream inputStream = new FileInputStream(file);
  125. byte[] chs = new byte[( int) file.length()];
  126. inputStream.read(chs);
  127. inputStream.close();
  128. String javaCode = new String(chs);
  129. String[] lines = javaCode.split( "\n");
  130. int find = lines.length; // 實際代碼行數
  131. int counts = find; // 加上注釋的行數
  132. int zhushi = 0;
  133. for ( int i = 0; i < lines.length; i++) {
  134. lines[i] = lines[i].trim();
  135. if (lines[i].length() == 0) {
  136. counts--;
  137. find--;
  138. } else if (lines[i].startsWith( "//")) {
  139. // System.out.println("單行注釋:"+lines[i]);
  140. find--;
  141. zhushi++;
  142. } else if (lines[i].indexOf( "/*") != - 1) {
  143. find--;
  144. zhushi++;
  145. while (lines[i].indexOf( "*/") == - 1) {
  146. // System.out.println(lines[i]);
  147. find--;
  148. zhushi++;
  149. i++;
  150. }
  151. }
  152. }
  153. double zc = (( double) zhushi / counts) * 100;
  154. BigDecimal b = new BigDecimal(zc);
  155. double zcc = b.setScale( 2, BigDecimal.ROUND_HALF_UP).doubleValue();
  156. String s = file.getName() + "代碼行數:" + find + "\t注釋行數:" + zhushi + "\t 注釋率:" + zcc + "%";
  157. contenList.add(s);
  158. }
  159. }
這一次果然快了很多,但是幅度不大,通過分析我發現然來C盤根目錄的文件夾也不是每一個大小都一樣的,有一些文件夾里面文件特別多,有一些就很少,而且只遍歷C盤根目錄,然后再調用多線程,可能文件夾數量沒有線程數多,我應該多遍歷幾層,再調用多線程,多次實驗后我發現只有遍歷三層才是最快的,原理不明,但是遍歷一層、兩層、四層、五層程序執行時間都比較長,三層是一個神奇的點。最后的實驗結果:多線程遍歷,文件越多約占優勢,16萬個文件,單線程遞歸算法需要8-9秒,單線程非遞歸需要7-8秒,三種結合只需要3-4秒,而且在文件數比較少的時候,此方法也有比較大幅度的提升,快一兩百毫秒。

最后貼上程序運行圖:




嗯,貼不了。。。

完整代碼就是上面那個,創建一個對象,給構造函數傳入文件路徑(String)和文件類型(String)就可以了。

小白之作,輕噴輕噴



原文地址:https://blog.csdn.net/qq_24833939/article/details/79222444


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM