`

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

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

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

01 publicclassTotalFileSizeSequential{
02 privatelonggetTotalSizeOfFilesInDir(finalFilefile){
03 if(file.isFile())returnfile.length();
04
05 finalFile[]children=file.listFiles();
06 longtotal=0;
07 if(children!=null)
08 for(finalFilechild:children)
09 total+=getTotalSizeOfFilesInDir(child);
10 returntotal;
11 }
12
13 publicstaticvoidmain(finalString[]args){
14 finallongstart=System.nanoTime();
15 finallongtotal=newTotalFileSizeSequential()
16 .getTotalSizeOfFilesInDir(newFile("D:/idea_ws"));
17 finallongend=System.nanoTime();
18 System.out.println("TotalSize:"+total);
19 System.out.println("Timetaken:"+(end-start)/1.0e9);
20 }
21 }

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

1 TotalSize:276589881
2 Timetaken:0.258706999



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

01 publicclassNaivelyConcurrentTotalFileSize{
02 privatelonggetTotalSizeOfFilesInDir(
03 finalExecutorServiceservice,finalFilefile)
04 throwsInterruptedException,ExecutionException,TimeoutException{
05 if(file.isFile())returnfile.length();
06
07 longtotal=0;
08 finalFile[]children=file.listFiles();
09
10 if(children!=null){
11 finalList<Future<Long>>partialTotalFutures=
12 newArrayList<Future<Long>>();
13 for(finalFilechild:children){
14 partialTotalFutures.add(service.submit(newCallable<Long>(){
15 publicLongcall()throwsInterruptedException,
16 ExecutionException,TimeoutException{
17 returngetTotalSizeOfFilesInDir(service,child);
18 }
19 }));
20 }
21
22 for(finalFuture<Long>partialTotalFuture:partialTotalFutures)
23 total+=partialTotalFuture.get(100,TimeUnit.SECONDS);
24 }
25
26 returntotal;
27 }
28
29 privatelonggetTotalSizeOfFile(finalStringfileName)
30 throwsInterruptedException,ExecutionException,TimeoutException{
31 finalExecutorServiceservice=Executors.newFixedThreadPool(100);
32 try{
33 returngetTotalSizeOfFilesInDir(service,newFile(fileName));
34 }finally{
35 service.shutdown();
36 }
37 }
38
39 publicstaticvoidmain(finalString[]args)
40 throwsInterruptedException,ExecutionException,TimeoutException{
41 finallongstart=System.nanoTime();
42 finallongtotal=newNaivelyConcurrentTotalFileSize()
43 .getTotalSizeOfFile("D:/idea_ws");
44 finallongend=System.nanoTime();
45 System.out.println("TotalSize:"+total);
46 System.out.println("Timetaken:"+(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 publicclassConcurrentTotalFileSize{
02 classSubDirectoriesAndSize{
03 finalpubliclongsize;
04 finalpublicList<File>subDirectories;
05 publicSubDirectoriesAndSize(
06 finallongtotalSize,finalList<File>theSubDirs){
07 size=totalSize;
08 subDirectories=Collections.unmodifiableList(theSubDirs);
09 }
10 }
11
12 privateSubDirectoriesAndSizegetTotalAndSubDirs(finalFilefile){
13 longtotal=0;
14 /*
15 list的作用:
16 1.先存放入参file,根据file,得到它下面的SubDirectoriesAndSize对象,即总的文件大小和总的文件夹的ls。根据getTotalSubDirs(finalFilefile)获取
17 2.清空上一步的入参file,用于存放SubDirectoriesAndSize.dirs
18 */
19 finalList<File>subDirectories=newArrayList<File>();
20 if(file.isDirectory()){
21 finalFile[]children=file.listFiles();
22 if(children!=null)
23 for(finalFilechild:children){
24 if(child.isFile())
25 total+=child.length();
26 else
27 subDirectories.add(child);
28 }
29 }
30 returnnewSubDirectoriesAndSize(total,subDirectories);
31 }
32
33 privatelonggetTotalSizeOfFilesInDir(finalFilefile)
34 throwsInterruptedException,ExecutionException,TimeoutException{
35 finalExecutorServiceservice=Executors.newFixedThreadPool(100);
36 try{
37 longtotal=0;
38 finalList<File>directories=newArrayList<File>();
39 directories.add(file);
40 while(!directories.isEmpty()){
41 finalList<Future<SubDirectoriesAndSize>>partialResults=
42 newArrayList<Future<SubDirectoriesAndSize>>();
43 for(finalFiledirectory:directories){
44 partialResults.add(
45 service.submit(newCallable<SubDirectoriesAndSize>(){
46 publicSubDirectoriesAndSizecall(){
47 returngetTotalAndSubDirs(directory);
48 }
49 }));
50 }
51 directories.clear();
52 for(finalFuture<SubDirectoriesAndSize>partialResultFuture:
53 partialResults){
54 finalSubDirectoriesAndSizesubDirectoriesAndSize=
55 partialResultFuture.get(100,TimeUnit.SECONDS);
56 directories.addAll(subDirectoriesAndSize.subDirectories);
57 total+=subDirectoriesAndSize.size;
58 }
59 }
60 returntotal;
61 }finally{
62 service.shutdown();
63 }
64 }
65
66 publicstaticvoidmain(finalString[]args)
67 throwsInterruptedException,ExecutionException,TimeoutException{
68 finallongstart=System.nanoTime();
69 finallongtotal=newConcurrentTotalFileSize()
70 .getTotalSizeOfFilesInDir(newFile("D:/idea_ws"));
71 finallongend=System.nanoTime();
72 System.out.println("TotalSize:"+total);
73 System.out.println("Timetaken:"+(end-start)/1.0e9);
74 }
75 }
以上代码多次运行后的结果为:
1 TotalSize:276592101
2 Timetaken:0.124100452

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

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


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

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

01 publicclassConcurrentTotalFileSizeWLatch{
02 privateExecutorServiceservice;
03 //用于计算待扫描的文件/子文件夹的个数
04 finalprivateAtomicLongpendingFileVisits=newAtomicLong();
05 finalprivateAtomicLongtotalSize=newAtomicLong();
06 //定义一个锁存器,给定计数初始化的CountDownLatch的个数是1个
07 finalprivateCountDownLatchlatch=newCountDownLatch(1);
08 privatevoidupdateTotalSizeOfFilesInDir(finalFilefile){
09 longfileSize=0;
10 if(file.isFile())
11 fileSize=file.length();
12 else{
13 finalFile[]children=file.listFiles();
14 if(children!=null){
15 for(finalFilechild:children){
16 if(child.isFile())
17 fileSize+=child.length();
18 else{
19 pendingFileVisits.incrementAndGet();
20 service.execute(newRunnable(){
21 publicvoidrun(){updateTotalSizeOfFilesInDir(child);}
22 });
23 }
24 }
25 }
26 }
27 totalSize.addAndGet(fileSize);
28 //递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
29 if(pendingFileVisits.decrementAndGet()==0)latch.countDown();
30 }
31
32 privatelonggetTotalSizeOfFile(finalStringfileName)
33 throwsInterruptedException{
34 service=Executors.newFixedThreadPool(100);
35 pendingFileVisits.incrementAndGet();
36 try{
37 updateTotalSizeOfFilesInDir(newFile(fileName));
38 //使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
39 latch.await(100,TimeUnit.SECONDS);
40 returntotalSize.longValue();
41 }finally{
42 service.shutdown();
43 }
44 }
45 publicstaticvoidmain(finalString[]args)throwsInterruptedException{
46 finallongstart=System.nanoTime();
47 finallongtotal=newConcurrentTotalFileSizeWLatch()
48 .getTotalSizeOfFile("D:/idea_ws");
49 finallongend=System.nanoTime();
50 System.out.println("TotalSize:"+total);
51 System.out.println("Timetaken:"+(end-start)/1.0e9);
52 }
53 }

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

1 TotalSize:276592105
2 Timetaken:0.114999062

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


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

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

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

第3版的代码如下:

01 publicclassConcurrentTotalFileSizeWQueue{
02 privateExecutorServiceservice;
03 //定义队列,存放每个线程得到的fileSize
04 finalprivateBlockingQueue<Long>fileSizes=
05 newArrayBlockingQueue<Long>(500);
06 //记录文件夹数
07 finalAtomicLongpendingFileVisits=newAtomicLong();
08 privatevoidstartExploreDir(finalFilefile){
09 pendingFileVisits.incrementAndGet();
10 service.execute(newRunnable(){
11 publicvoidrun(){exploreDir(file);}
12 });
13 }
14 privatevoidexploreDir(finalFilefile){
15 longfileSize=0;
16 if(file.isFile())
17 fileSize=file.length();
18 else{
19 finalFile[]children=file.listFiles();
20 if(children!=null)
21 for(finalFilechild: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(Exceptionex){thrownewRuntimeException(ex);}
32 pendingFileVisits.decrementAndGet();
33 }
34
35 privatelonggetTotalSizeOfFile(finalStringfileName)
36 throwsInterruptedException{
37 service=Executors.newFixedThreadPool(100);
38 try{
39 startExploreDir(newFile(fileName));
40 longtotalSize=0;
41 while(pendingFileVisits.get()>0||fileSizes.size()>0)
42 {
43 finalLongsize=fileSizes.poll(10,TimeUnit.SECONDS);
44 totalSize+=size;
45 }
46 returntotalSize;
47 }finally{
48 service.shutdown();
49 }
50 }
51 publicstaticvoidmain(finalString[]args)throwsInterruptedException{
52 finallongstart=System.nanoTime();
53 finallongtotal=newConcurrentTotalFileSizeWQueue()
54 .getTotalSizeOfFile("D:/idea_ws");
55 finallongend=System.nanoTime();
56 System.out.println("TotalSize:"+total);
57 System.out.println("Timetaken:"+(end-start)/1.0e9);
58 }
59 }

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

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

相关推荐

    易语言多线程扫描文件夹

    总的来说,易语言多线程扫描文件夹是一种提高文件系统操作效率的方法,通过合理分配任务和有效管理线程,可以在保证程序响应速度的同时,提升大文件夹扫描的性能。在实际应用中,根据具体的场景和需求,我们还可以...

    VB.NET 多线程列出文件夹大小

    总结,"VB.NET 多线程列出文件夹大小"项目结合了多线程、文件系统操作、UI控件使用等技术,旨在高效且用户友好地呈现文件夹的大小信息。在实践中,需要考虑线程管理、错误处理和用户体验优化,以确保程序的稳定性和...

    c#文件扫描递归方法实现

    在这个场景下,我们可以创建一个功能强大的多线程解决方案,提高文件扫描的效率。 首先,让我们深入理解递归的概念。递归是一种函数或过程调用自身的技术,通常用于解决具有层次结构的问题,如遍历目录结构。在C#中...

    磁盘和文件夹扫描vb.net

    4. **进度条与多线程**:对于大文件夹或大量文件,显示扫描进度和使用多线程技术是提升用户体验的关键。`BackgroundWorker`组件可以用来异步执行扫描任务,同时更新UI上的进度条,避免主线程阻塞导致界面无响应。 5...

    C语言实现文件扫描

    - 对于非常大的文件系统,应考虑分批处理或者使用多线程/多进程技术提高效率。 - 使用标准库函数时,需注意不同平台之间的兼容性问题。 综上所述,通过C语言实现文件夹的递归扫描不仅能够清晰地展示文件系统的层次...

    文件 清理 搜索 多线程 快速 C#

    总的来说,这个项目展示了如何利用C#的多线程和文件操作功能来实现一个高效的文件清理和搜索工具。对于初学者和经验丰富的开发者来说,这是一个很好的实践案例,可以深入理解多线程编程和文件系统操作。同时,通过对...

    VB.NET磁盘和文件夹扫描程序源代码

    综上所述,这个VB.NET磁盘和文件夹扫描程序源代码涵盖了文件系统的操作、遍历、过滤、异步处理等多个核心概念。学习并理解这段代码,开发者可以掌握如何在实际项目中实现类似的文件系统操作功能。

    全盘查找文件夹 源码

    因此,优化搜索算法(如使用多线程并行查找)和提供用户界面来限制搜索范围或取消操作,都是必要的。 至于压缩包中的“查找固定目录”,这可能是指源码中包含一个固定的起始搜索目录,比如C:\ 或者 /home 用户目录...

    用C#写的扫描文件程序

    例如,使用缓存避免重复扫描,或者使用多线程并行扫描不同目录以提高速度。 9. **安全考虑**:在访问用户文件系统时,程序应遵循最小权限原则,只读取必要的信息,不修改或删除任何文件,以保护用户数据的安全。 ...

    目录多文件上传-JAVA IO流常用详解

    该功能可以实现在用户选择一个目录后,自动扫描并上传该目录下的所有文件及其子文件夹,最终达到完整备份或上传的目的。 #### 核心知识点 1. **目录多文件上传的基本原理** - 本功能主要基于递归思想,通过遍历...

    java文件夹上传

    这个组件可能包括了文件夹扫描、文件分割、多线程上传、错误处理和进度反馈等功能。使用时,开发者只需调用适当的接口,提供需要上传的文件夹路径和目标服务器地址,即可启动上传过程。 总结起来,Java文件夹上传...

    遍历文件夹

    6. **并发处理**:在多核处理器的系统上,可以考虑使用多线程或多进程同时遍历不同的文件夹分支,以进一步提升速度。 7. **错误处理**:遍历过程中可能会遇到权限问题、文件被占用无法访问等情况,因此需要编写恰当...

    自动扫描文件

    在自动扫描文件夹时,可以使用`System.IO`命名空间中的`Directory`和`FileInfo`类。`Directory.GetFiles()`方法可以遍历指定路径下的所有文件,而`FileInfo`类则提供了文件的基本信息,如大小、创建时间等。 文件...

    Java监控目录文件夹程序.rar

    在实际应用中,程序可能会使用多线程技术,一个线程负责持续扫描目录,另一个线程负责更新用户界面。这样可以确保程序的响应性,避免因为文件扫描操作阻塞UI。同时,为了减少资源消耗,开发者可能还使用了延迟或节拍...

    扫描播放视频demo

    同时,为了提高效率,还可以利用多线程或异步处理来并发扫描多个目录。 接下来,我们讨论视频播放。视频播放涉及到解码、渲染等多个步骤。当视频文件被选择后,播放器首先需要读取文件头,获取视频的编码格式、...

    文件夹遍历

    在设计这样的工具时,还需要考虑到性能优化,如使用多线程、缓存结果、避免重复扫描等策略。 总之,文件夹遍历是编程中常见的任务,掌握这一技能有助于更好地处理文件系统相关的任务。无论是简单的显示目录结构,...

    c#系统监控软件,可以监控全盘文件及其子文件夹

    文件监控软件的实现可能还会涉及多线程和异步编程,以确保在监控大量文件时不会阻塞UI,同时提高响应速度。此外,为了优化效率和降低资源消耗,可能会采用定时扫描或事件驱动的混合策略。 总之,这个C#系统监控软件...

    具有粘贴,整个文件夹,多文件上传的控件demo(java)

    9. **性能优化**:对于大量文件的上传,可能需要考虑分块上传、断点续传、多线程处理等技术来提高上传效率。 10. **用户界面**:良好的用户界面设计是关键,包括直观的进度指示、错误提示、取消上传等功能,以提升...

    FFS.rar_FFS_FFS java

    总的来说,FFS项目是一个综合性的Java应用,它涵盖了网络编程、多线程、文件操作、权限控制等多个关键领域,是学习和提升Java编程技能的好例子。通过分析和研究FFS的源代码,开发者不仅可以学习到具体的编程技巧,还...

Global site tag (gtag.js) - Google Analytics