多线程IO操作(扫描文件夹并计算总大小)

场景为,给到一个硬盘上文件或文件夹,(当然文件夹时,多线程的优势才能越发体现出来),得到该文件或文件夹的大小和计算该结果所需要的时间。

首先是单线程下的例子,这个可难不倒大家,代码如下:

01 public class TotalFileSizeSequential {
02   private long getTotalSizeOfFilesInDir(final File file) {
03     if (file.isFile()) return file.length();
04      
05     final File[] children = file.listFiles();
06     long total = 0;
07     if (children != null)
08       for(final File child : children) 
09         total += getTotalSizeOfFilesInDir(child);      
10     return total;
11   }
12    
13   public static void main(final String[] args) {
14     final long start = System.nanoTime();
15     final long total = new TotalFileSizeSequential()
16       .getTotalSizeOfFilesInDir(new File("D:/idea_ws"));
17     final long end = System.nanoTime();
18     System.out.println("Total Size: " + total);
19     System.out.println("Time taken: " + (end - start)/1.0e9);
20   }
21 }

上述代码在我的机器上(win7,8g,i5)多次运行后的均值为:文件夹大小为276590351字节,即263M;耗时大概 0.25s

1 Total Size: 276589881
2 Time taken: 0.258706999

至此,我们当然希望通过多线程扫描计算的方式,来更快的得到这个文件夹的大小,于是有个这个看似设计不错的,native版的代码:

01 public class NaivelyConcurrentTotalFileSize {
02   private long getTotalSizeOfFilesInDir(
03     final ExecutorService service, final File file) 
04     throws InterruptedException, ExecutionException, TimeoutException {
05     if (file.isFile()) return file.length();
06      
07     long total = 0;
08     final File[] children = file.listFiles();
09      
10     if (children != null) {
11       final List<Future<Long>> partialTotalFutures = 
12         new ArrayList<Future<Long>>();      
13       for(final File child : children) {
14         partialTotalFutures.add(service.submit(new Callable<Long>() {
15           public Long call() throws InterruptedException, 
16             ExecutionException, TimeoutException { 
17             return getTotalSizeOfFilesInDir(service, child); 
18           }
19         }));
20       }
21        
22       for(final Future<Long> partialTotalFuture : partialTotalFutures) 
23         total += partialTotalFuture.get(100, TimeUnit.SECONDS);
24   }
25      
26     return total;
27   }
28  
29   private long getTotalSizeOfFile(final String fileName)
30     throws InterruptedException, ExecutionException, TimeoutException {
31       final ExecutorService service = Executors.newFixedThreadPool(100);
32       try {
33         return getTotalSizeOfFilesInDir(service, new File(fileName));
34       finally {
35           service.shutdown();
36       }
37   }
38      
39   public static void main(final String[] args) 
40     throws InterruptedException, ExecutionException, TimeoutException {
41     final long start = System.nanoTime();
42     final long total = new NaivelyConcurrentTotalFileSize()
43       .getTotalSizeOfFile("D:/idea_ws");
44     final long end = System.nanoTime();
45     System.out.println("Total Size: " + total);
46     System.out.println("Time taken: " + (end - start)/1.0e9);
47   }
48 }

额,运行的结果出乎意料,多次运行该多线程代码,去扫描计算这个263M的文件夹时,仅有几次能运行成功,大部分时候,都是死锁,导致程序崩溃,根本无法计算文件夹大小。

这段程序在getTotalSizeOfFilesInDir(final ExecutorService service, final File file) 方法中,有阻塞线程池的操作。每当扫描到一个子目录的时候,它就将该任务调度给其他的线程。这个逻辑看上去没有错,一旦它调度完了所有任务,该函数就等待任何一个任务的响应。即是代码中的 total += partialTotalFuture.get(100, TimeUnit.SECONDS);这一行。

当然,当给到扫描的目录不是很深,文件不很多时,这样做确实能很快的得到结果,否则,就会在该行卡主,一直等待响应,若是没有我们设置的100的超时操作,这将演变成一种潜在的“线程诱发型死锁(Pool Include DeadLock)”

此时,我们就要考虑如何改善这段代码了,目标是:计算各子目录大小的任务分配给不同线程,当我们等待其他任务响应时,又不会占住当前的主调线程(避免死锁)。

下面,是第一种解决方案:每个任务都返回给定目录的直接子目录列表,而不是去返回计算的大小。这样的好处是,使线程被堵住的时间不会超过扫描给定目录的直接子目录的时间。在返回直接子目录的同时,计算出目录中的文件的大小一并返回。

那么,第一个修复版本的代码如下:

01 public class ConcurrentTotalFileSize {
02   class SubDirectoriesAndSize {
03     final public long size;
04     final public List<File> subDirectories;
05     public SubDirectoriesAndSize(
06       final long totalSize, final List<File> theSubDirs) {
07       size = totalSize;
08       subDirectories = Collections.unmodifiableList(theSubDirs);
09     }
10   }
11  
12   private SubDirectoriesAndSize getTotalAndSubDirs(final File file) {
13     long total = 0;
14   /*
15                 list的作用:
16                 1.先存放入参file,根据file,得到它下面的SubDirectoriesAndSize对象,即总的文件大小和总的文件夹的ls。根据getTotalSubDirs(final File file)获取
17                 2.清空上一步的入参file,用于存放SubDirectoriesAndSize.dirs
18              */
19     final List<File> subDirectories = new ArrayList<File>();
20     if(file.isDirectory()) {
21       final File[] children = file.listFiles();
22       if (children != null)
23         for(final File child : children) {
24           if (child.isFile())
25             total += child.length();
26           else
27             subDirectories.add(child);
28         }
29     }
30     return new SubDirectoriesAndSize(total, subDirectories);
31   }
32  
33   private long getTotalSizeOfFilesInDir(final File file) 
34     throws InterruptedException, ExecutionException, TimeoutException {
35     final ExecutorService service = Executors.newFixedThreadPool(100);
36     try {
37       long total = 0;
38       final List<File> directories = new ArrayList<File>();
39       directories.add(file);
40       while(!directories.isEmpty()) {
41         final List<Future<SubDirectoriesAndSize>> partialResults = 
42           new ArrayList<Future<SubDirectoriesAndSize>>();
43         for(final File directory : directories) {
44           partialResults.add(
45             service.submit(new Callable<SubDirectoriesAndSize>() {
46             public SubDirectoriesAndSize call() {
47               return getTotalAndSubDirs(directory);
48             }
49           }));
50         }
51         directories.clear();     
52         for(final Future<SubDirectoriesAndSize> partialResultFuture : 
53           partialResults) {
54           final SubDirectoriesAndSize subDirectoriesAndSize = 
55             partialResultFuture.get(100, TimeUnit.SECONDS);
56           directories.addAll(subDirectoriesAndSize.subDirectories);
57           total += subDirectoriesAndSize.size;
58         }
59       }
60       return total;
61     finally {
62       service.shutdown();
63     }
64   }
65  
66   public static void main(final String[] args) 
67     throws InterruptedException, ExecutionException, TimeoutException {
68     final long start = System.nanoTime();
69     final long total = new ConcurrentTotalFileSize()
70       .getTotalSizeOfFilesInDir(new File("D:/idea_ws"));
71     final long end = System.nanoTime();
72     System.out.println("Total Size: " + total);
73     System.out.println("Time taken: " + (end - start)/1.0e9);
74   }
75 }

以上代码多次运行后的结果为:

1 Total Size: 276592101
2 Time taken: 0.124100452

比起单线程版本,速度基本提升了一倍的样子。到此,我们这个任务就完成了么?不,难道你不觉得上面这个设计,虽然说起来简单,但是实现起来却并不那么容易么?

我们不仅需要设计一个用于保存计算任务的返回结果的不变的子类,而且还要花心思去设计如何不断的分派任务以及协调处理任务的返回结果。所以,即使我们这个设计提高了性能,却引入了相当的复杂性。

所以,我们下面将会引入CountDownLatch辅助实现线程的设计和协作。上面的设计中,我们引入了Future,我们通过它获取任务的执行结果,同时,Future也隐含的帮我们完成了一些任务/线程之间的协调工作,我们因此不用考虑线程切换和并行中可能发现的一些问题。但是,大家知道,Future是需要返回值的,假使我们的任务没有返回值的话,Future就不太适合出现在我们的代码中,并且作为线程间的协作工具了。此时,我们会考虑到CountDownLatch作为替代。

下面,我们将使用到CountDownLatch去做协调,在扫描到一个文件时,线程不在能够通过Future去接受一个计算的返回结果,而是去更新一个AtomicLong类型的共享变量totalSize。AtomicLong提供了更新并取回一个简单long类型的变量的线程安全的方法。此外,我们还会有个AtomicLong类型的变量pendingFileVisits,用于保存当前等待访问的文件或子目录的数量。而当该值为0时,我们就通过countDown()来释放线程闩。

01 public class ConcurrentTotalFileSizeWLatch {  
02   private ExecutorService service;
03    //用于计算待扫描的文件/子文件夹的个数
04   final private AtomicLong pendingFileVisits = new AtomicLong();
05   final private AtomicLong totalSize = new AtomicLong();
06     //定义一个锁存器,给定计数初始化的 CountDownLatch的个数是1个
07   final private CountDownLatch latch = new CountDownLatch(1);
08   private void updateTotalSizeOfFilesInDir(final File file) {
09     long fileSize = 0;
10     if (file.isFile())
11       fileSize = file.length();
12     else {      
13       final File[] children = file.listFiles();      
14       if (children != null) {
15         for(final File child : children) {
16           if (child.isFile()) 
17             fileSize += child.length();
18           else {
19             pendingFileVisits.incrementAndGet();
20             service.execute(new Runnable() {
21               public void run() { updateTotalSizeOfFilesInDir(child); }
22             });            
23           }
24         }
25       }
26     }
27     totalSize.addAndGet(fileSize);
28     //递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
29     if(pendingFileVisits.decrementAndGet() == 0) latch.countDown();
30   }
31  
32   private long getTotalSizeOfFile(final String fileName) 
33     throws InterruptedException {
34     service  = Executors.newFixedThreadPool(100);
35     pendingFileVisits.incrementAndGet();
36     try {
37      updateTotalSizeOfFilesInDir(new File(fileName));
38         // 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
39      latch.await(100, TimeUnit.SECONDS);
40      return totalSize.longValue();
41     finally {
42       service.shutdown();
43     }
44   }
45   public static void main(final String[] args) throws InterruptedException {
46     final long start = System.nanoTime();
47     final long total = new ConcurrentTotalFileSizeWLatch()
48       .getTotalSizeOfFile("D:/idea_ws");
49     final long end = System.nanoTime();
50     System.out.println("Total Size: " + total);
51     System.out.println("Time taken: " + (end - start)/1.0e9);
52   }
53 }

这个版本的代码比较上一个版本,简洁了很多,下面我们看看运行的结果:

1 Total Size: 276592105
2 Time taken: 0.114999062

结果比较上一个版本,性能上差不多。然而,即使简洁,高性能,但是这个版本的代码存在着访问共享可变变量的风险。我们将在后面研究,如何二者兼得的方法。

上面2个版本的代码中,我们通过Future和AtomicLong来实现数据的交换功能。当任务完成时能获取返回值时,Future就能派上用场,而AtomicLong的线程安全的原子操作对处理单个共享数据的值来说,是非常有用的。

如果,只是想在两个线程间交换数据,我们可以用Exchanger类。它可以看做一个同步点,两个线程在改同步点上,可以线程安全的方式互换数据,如果两个线程的运行速度不一样,则运行较快的会被阻塞,直到慢的线程赶到同步点时,才能开始数据的互换。

所以,如果想在线程间互发多组数据,则BlockingQueue接口可以派上用场。队列想必大家都比较熟悉了,队列没用空间,插入时会被阻塞;队列里没有可用数据,删除时会被阻塞。若想要插入和删除操作一一对应,可以使用SynchronousQueue类。该类的作用是本线程的每一个插入操作与其他线程相应的删除操作相匹配,以完成类似的手递手形式的数据传输。如果希望数据根据某种优先级在队列中上下浮动,则可以使用PriorityBlockingQueue。另外,如果只是要一个简单的阻塞队列,我们可以用链表的实现LinkedBlockingQueue或者数组的实现ArrayBlockingQueue。

第3版的代码如下:

01 public class ConcurrentTotalFileSizeWQueue {  
02   private ExecutorService service;
03   //定义队列,存放每个线程得到的fileSize
04   final private BlockingQueue<Long> fileSizes = 
05     new ArrayBlockingQueue<Long>(500);
06     //记录文件夹数
07   final AtomicLong pendingFileVisits = new AtomicLong();
08   private void startExploreDir(final File file) {
09     pendingFileVisits.incrementAndGet();
10     service.execute(new Runnable() {
11       public void run() { exploreDir(file); }
12     });          
13   }
14   private void exploreDir(final File file) {
15     long fileSize = 0;
16     if (file.isFile())
17       fileSize = file.length();
18     else {
19       final File[] children = file.listFiles();
20       if (children != null)
21         for(final File child : children) {
22           if (child.isFile())
23             fileSize += child.length();
24           else {
25             startExploreDir(child);
26           }
27         }
28     }
29     try 
30       fileSizes.put(fileSize); 
31     catch(Exception ex) { throw new RuntimeException(ex); }
32     pendingFileVisits.decrementAndGet();          
33   }
34  
35   private long getTotalSizeOfFile(final String fileName) 
36     throws InterruptedException {
37     service  = Executors.newFixedThreadPool(100);
38     try {
39       startExploreDir(new File(fileName));
40       long totalSize = 0;
41       while(pendingFileVisits.get() > 0 || fileSizes.size() > 0)
42       {
43         final Long size = fileSizes.poll(10, TimeUnit.SECONDS);
44         totalSize += size;
45       }
46       return totalSize;
47     finally {
48       service.shutdown();
49     }
50   }   
51   public static void main(final String[] args) throws InterruptedException {
52     final long start = System.nanoTime();
53     final long total = new ConcurrentTotalFileSizeWQueue()
54       .getTotalSizeOfFile("D:/idea_ws");
55     final long end = System.nanoTime();
56     System.out.println("Total Size: " + total);
57     System.out.println("Time taken: " + (end - start)/1.0e9);
58   }
59 }

这个版本的运行结果如下:

1 Total Size: 276591906
2 Time taken: 0.124069344

这一版的性能与之前一版相仿,但是代码简化方面又有提升了,主要归功与阻塞队列帮我们完成了线程之间的数据交换和同步的操作。由于oschina博客的字数限制,之后一篇文章,将利用java7引入的新的api来进一步改进上面的方案

多线程IO操作(扫描文件夹并计算总大小)

时间: 2024-08-26 07:16:25

多线程IO操作(扫描文件夹并计算总大小)的相关文章

java io流 对文件夹的操作

检查文件夹是否存在 显示文件夹下面的文件 ....更多方法参考 http://www.cnblogs.com/phpyangbo/p/5965781.html ,与文件操作方法通用,因为都是一个类 //对文件夹的操作 //检查文件夹是否存在 //显示文件夹下面的文件 //.... import java.io.*; import java.util.*; public class Index{ public static void main(String[] args) throws Excep

多线程IO操作(fork-join版)

接着上篇中没写完的(http://my.oschina.net/bluesroot/blog/223453),上篇中讲到很多,为完成对一个目录的扫描的频繁的IO操作,我们从单线程到多线程,从CountDownLatch到BlockingQueue,中间不免各种Callable和Future和ExecutorService等等,虽然纷繁,中间有些不免麻烦,但是最终仍紧紧贴着我们的需求和多线程操作这一主题. 随着jdk的版本升级,并发包也随之扩充了不少,上面博文中的API来源于jdk1.6,1.7的

java扫描文件夹下面的所有文件(递归与非递归实现)

java中扫描指定文件夹下面的所有文件扫描一个文件夹下面的所有文件,因为文件夹的层数没有限制可能多达几十层几百层,通常会采用两种方式来遍历指定文件夹下面的所有文件.递归方式非递归方式(采用队列或者栈实现)下面我就给出两种方式的实现代码,包括了递归与非递归实现,code如下所示. package q.test.filescanner; import java.io.File;import java.util.ArrayList;import java.util.LinkedList; import

使用IO流复制文件夹(包括子目录)

IO流用于处理设备上的数据(包括硬盘,内存,键盘录入). IO流可根据流向不同分为输入流和输出流,根据处理数据类型不同分为字节流和字符流. 字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象.本质其实就是基于字节流读取时,去查了指定的码表. 字节流和字符流的区别: a.读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节. b.处理对象不同:字节流能处理所有类型的数据(如图片.avi等),而字符流只能处理字符类型的数据. 结论:

扫描文件夹下代码行数

突发奇想,     想知道项目里有多少行代码.花了一点时间写了个初步的demo,原理很简单,扫描项目src目录,拿到所有的文件按行读取,每读取一行,累计加1. 难      点:    如何跳过目录以及获得目录下的文件和下一级目录,这里用递归比较好. 不足之处:    不支持识别注释,不支持识别代码提交者,不支持时间分类,文件类型分类,不支持总字符统计. 改进方式:   单行注释比较容易,trim一下判断是否以 // 开头,或者用正则匹配一下,多行就有点麻烦了,可能要读取整个文件内容,然后通过正

递归实现显示目标文件夹的所有文件和文件夹,并计算目标文件夹的大小

递归的一个典型应用就是遍历目标文件夹,把该文件夹下的所有文件和文件夹打印或显示出来,还可以递归计算目标文件夹的总大小. 1: class Program 2: { 3: static void Main(string[] args) 4: { 5: Console.WriteLine("输入目标文件夹"); 6: string path = Console.ReadLine(); 7: FindFoldersAndFiles(path); 8: Console.WriteLine(&q

使用IO流对文件夹的复制(文件夹中带有子目录)

当我们要复制带有子目录的文件夹的时候,就必须使用递归,这样才能把全部的文件夹都复制到位 思路和步骤: 对于文件夹的复制,分2种情况               (1)当我们对文件进行遍历的时候,如果目标文件夹下的文件是个标准文件的话,我们就可以直接去做复制的动作,               (2)当目标文件中,是带有文件夹的文件,那么这个时候就需要对文件夹进行递归,直到最后它是个标准文件后,我们在做复制动作 有了上述的2种情况后,那么这个需求,需要提供2种方法,1是对标准文件的复制,2是对带有

QT设计界面遍历文件夹进而计算hash码

这个星期的主要任务是用QT5.2.0+vs2012设计一个界面,提供一个按钮,点击之后弹出一个选择目录的窗口,然后选择一个指定的目录,选择好之后计算这个目录下面的所有文件的hash,然后显示出来. 设计的主要思路: 1.创建Dialog基类的工程,添加QPushButton.QLineEdit和QTextEdit的控件,并通过connect将其关联: 2.利用QFileDialog::getExistingDirectory得到一个Qir类的文件夹信息fileinfo,将文件夹路径fileinf

第5个程序,Java 去掉版权信息! 递归扫描文件夹,并且把整个文件夹内以html结尾的文件里的文字替换为自己想要的文字。

这是我的第5个程序 虽然这五个程序都是非常小的程序,但是写完后很开心...满满都是最最最基础的知识点,可是把这些东西组合起来简直日了狗了.竟然可以这样!竟然会那样!一路改错,改到差点怀疑人生.尤其是递归里面的两个嵌套的if 那逻辑!日了整个地球的狗.起先是用&连起来的,卡了半年,胡子都白了...后来发现,原来被别的地方锁死了!!!所以只能if 里面套一个if...看上去并没有什么特别的一个小程序,自己纯原创写的时候,150ms的延迟,1500的暴击,满地图都卡.死了...让我平复一下激动兴奋的心