首页 > 代码库 > 多线程的对比与案例(计算目录下文件的大小)

多线程的对比与案例(计算目录下文件的大小)

本人使用的是mac 所以有usr目录。把以下的几种情况分别贴出来给大家分析下各自有什么优缺点!

1.顺序计算目录大小code:

package jvm;

import java.io.File;

/**
 * 第一版
 * 顺序计算目录大小
 * @author zeuskingzb
 *
 */
public class TotalFileSizeSequential {
	private long getTotalSizeOfFilesInDir(File file){
		if (file.isFile()) {
			return file.length();
		}
		final File[] children = file.listFiles();
		long total = 0;
		if (children != null) {
			for (File child : children) {
				total += getTotalSizeOfFilesInDir(child);
			}
		}
		return total;
	}
	public static void main(String[] args) {
		final long  start = System.nanoTime();
		final long total = new TotalFileSizeSequential().getTotalSizeOfFilesInDir(new File("/usr"));
		final long end = System.nanoTime();
		System.out.println("Total size :"+total);
		System.out.println("Time taken:"+ (end-start)/1.0e9);
	}
}

第一次运行的结果:

Total size :2912829118

Time taken:6.263514

第二次运行的结果:

Total size :2912829118

Time taken:0.747395


在本代码中,首先我们给定一个目录,然后递归地累加其中所有文件/子目录的大小。
和本书中其他计算目录大小的程序一样,上面的代码第一次执行需要花很长时间,但如果在几分钟内再执行一次的话则耗时将会有所下降。这里因为在程序执行一次之后,文件系统的文件/目录信息缓存在内存里,从而加快了程序的执行速度,我忽略了程序第一次执行的时间,以便所有的比较测试都利用系统缓存的加速效果。


2.我们希望通过将上述code进行并发改造来加快执行速度。于是我们将问题拆分成若干子任务。其中每个子任务负责一个子目录/文件的计算并返回其统计结果。Callable接口是个不错的选择,因为该接口在call()函数可以在任务完成之后返回一个结果值。当程序循环扫描目录下的每个文件/目录的时候,我们就可以使用ExecutorService的submit()函数来调度子任务执行计算工作。随后我们可以调用Future对象的get()函数来获取子任务的计算结果,该对象主要起到一个委托的作用,即子任务一完成它就将结果返回给我们。
package jvm;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * 第二版
 * 执行 出现了 超时
 * 速度慢的原因都把时间花在线程调度上面了
 * 待扫描的目录结构很深,则程序 就会卡在这个地方
 * 
 * 
 * 
 *   Callable接口类似于Runnable,从名字就可以看出来了,
 *   但是Runnable不会返回结果,并且无法抛出返回结果的异常,
 *   而Callable功能更强大一些,被线程执行后,可以返回值,
 *   这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值
 * @author zeuskingzb
 *
 */
public class NaivelyConcurrentTotalFileSize {
	private long getTotalSizeOfFilesInDir(final ExecutorService service,final File file) throws InterruptedException, ExecutionException, TimeoutException{
		if (file.isFile()) {
			return file.length();
		}
		long total = 0;
		final File[] children = file.listFiles();
		if (children != null) {
			
			final List<Future<Long>> partialTotalFutures = new ArrayList<Future<Long>>();
			for (final File child : children) {
				partialTotalFutures.add(service.submit(new Callable<Long>() {
					@Override
					public Long call() throws Exception {
						// TODO Auto-generated method stub
						
						return getTotalSizeOfFilesInDir(service, child);
					}
				}));
			}
			for (Future<Long> partialTotalFuture : partialTotalFutures) {
				//设置 超时时间100ms
					total += partialTotalFuture.get(100, TimeUnit.SECONDS);
			}
			
		}
		return total;
	}
	private long getTotalSizeOfFile(final String fileName) throws InterruptedException, ExecutionException, TimeoutException{
		// 100 线程池
		final ExecutorService service = Executors.newFixedThreadPool(100);
			return getTotalSizeOfFilesInDir(service, new File(fileName));
	}
	
	public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
		final long start = System.nanoTime();
		final long total = new NaivelyConcurrentTotalFileSize().getTotalSizeOfFile("/usr");
		final long end = System.nanoTime();
		System.out.println("Total size :"+total);
		System.out.println("Time taken:"+ (end-start)/1.0e9);
	}
}

运行结果:

Exception in thread "main" java.util.concurrent.TimeoutException

at java.util.concurrent.FutureTask.get(FutureTask.java:201)

at jvm.NaivelyConcurrentTotalFileSize.getTotalSizeOfFilesInDir(NaivelyConcurrentTotalFileSize.java:51)

at jvm.NaivelyConcurrentTotalFileSize.getTotalSizeOfFile(NaivelyConcurrentTotalFileSize.java:60)

at jvm.NaivelyConcurrentTotalFileSize.main(NaivelyConcurrentTotalFileSize.java:65)




此任务容易超时,速度比上个版本的慢,这是因为很多时间被消耗在线程调度上.
在getTotalSizeOfFileInDir()函数中有阻塞线程池的操作。每当扫描到一个子目录的时候getTotalSizeOfFileInDir()函数就将扫描该子目录的任务调度给其他线程。一旦它调度完了所有任务,该函数就等待任何一个任务的响应。当子目录数据不是很多的时候,这种做法不会有什么问题。但是如果待扫描的目录结构很深, 则程序就会卡在这个地方。即线程池内的线程在等待某些任务的响应,而这些任务却在ExecutorService的队列中等待执行机会(由于程序是递归且线程池大小是固定的,所以当子目录数超过线程池大小时,就会发生所有线程都 在等待最底层目录的计算结果,而最底层子目录的计算任务又没有额外的线程来执行,以至形成死锁。)如果我们没有设置超时的话,这将演变成一种潜在的"线程池诱发型死锁"。由于设置了超时时间,所以我们至少 能够在出问题的时候中断程序运行而不是无休止地等下去。看来顺序版本的代码并发化并不是一件简单的事。

3.令每个任务都返回给目录的子目录列表而非该目录的大小。于是在主任务中,我们就可以分派其他任务来扫描列表中的子目录。该方法的好处是使线程被堵住的时候不会走超过扫描给事实上目录的直接子目录的时间。当每个任务返回给定目录的子目录列表的时候,也会把该目录中所含文件的大小算好一并返回


package jvm;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;



/**
 * 第三版
 * 速度比第四版要快点 
 * 
 * 此版本的代码逻辑比顺序执行的实现要复杂很多,相比于旧的Naively,这个版本在设计上更N x
 * 由于这个版本的避免了潜在在死锁的问题,并能快速获取了目录列表,所以我们可以独立地调度线程来逐个遍历这些个目录。
 * @author zeuskingzb
 *
 */
public class ConcurrentTotalFileSize {
	//内部类
	class SubDirectoriesAndSize{
		final public long size;
		final public List<File> subDirectories;//了文件列表
		public SubDirectoriesAndSize(final long totalSize,final List<File> theSubDirs){
			size = totalSize;
			subDirectories = Collections.unmodifiableList(theSubDirs);//得到数据,也就是个只读的数据,不能更改
		}
	}
	/**
	 * 得到一个包含该目录下所以子目录的列表,和该目录下所有文件大小的SubDirectoriesAndSize对象
	 * @param file
	 * @return
	 */
	private SubDirectoriesAndSize getTotalAndSubDirs(final File file){
		long total = 0;
		final List<File> subDirectories = new ArrayList<File>();
		if (file.isDirectory()) {//如果是目录
			final File[] children = file.listFiles();
			if (children != null) {
				for (File child : children) {
					if (child.isFile()) {
						total += child.length();//如何是文件则统计总和
					}else{
						subDirectories.add(child);//如果是目录的话放入目录列表
					}
				}
			}
		}
		return new SubDirectoriesAndSize(total, subDirectories);
	}
	
	private long getTotalSizeOfFilesInDir(final File file) throws InterruptedException, ExecutionException, TimeoutException{
		final ExecutorService serive = Executors.newFixedThreadPool(100);
		try {
			long total = 0;
			final List<File> directories = new ArrayList<File>();
			directories.add(file);//目录
			while (!directories.isEmpty()) {//不为空
				//接收内部类的列表
				final List<Future<SubDirectoriesAndSize>> partialResults = new ArrayList<Future<SubDirectoriesAndSize>>();
				for (final File directory : directories) {
					//多线程
					partialResults.add(serive.submit(new Callable<SubDirectoriesAndSize>() {

						@Override
						public SubDirectoriesAndSize call() throws Exception {
							// TODO Auto-generated method stub
							return getTotalAndSubDirs(directory);
						}
					} ));
				}
				//清列表,directories只是工具,数据在partialResults里面
				directories.clear();
				
				for (Future<SubDirectoriesAndSize> partialResultFuture : partialResults) {
					final SubDirectoriesAndSize subDirectoriesAndSize = partialResultFuture.get(100, TimeUnit.SECONDS);
					//System.out.println(directories.size());
					//把目录列表给目录
					directories.addAll(subDirectoriesAndSize.subDirectories);
					System.out.println("size:  "+subDirectoriesAndSize.size);
					total += subDirectoriesAndSize.size;
				}
			}
			return total;
		} finally{
			serive.shutdown();
		}
		
	}
	public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
		final long start = System.nanoTime();
		final long total = new ConcurrentTotalFileSize().getTotalSizeOfFilesInDir(new File("/usr"));
		final long end = System.nanoTime();
		System.out.println("Total size :"+total);
		System.out.println("Time taken:"+ (end-start)/1.0e9);
	}
}

执行结果 :

Total size :2912829118

Time taken:0.498055

4.下面的实现中,我们不再像之前那样返回子目录列表和文件大小,而是令每个线程都去更新一个共享变量。由于没有任何返回值,代码较之前大大简化。同样,我们必须保证主线程要等待所有子目录遍历完成之后才能结束。为此,我们可以使用CountDownLatch作为等待结束的标记。线程闩的作用是作为一个或多个线程等待其他线程到达其完成位置的同步点,这里我们简单地将其作为一个开关来使用。

package jvm;

import java.io.File;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 第四版
 * 每个线程都去更新一个共享变量。
 * 我们必须保证主线程要等待所有了目录遍历完成之后才能结束。
 * 我们在这里使用 CountDownLatch 作为等待结束的标记,Latch的作用是作为一个
 * 或多个线程等待其他线程到达其完成位置的同步点,这里我们简单地将其作为一个开关使用
 * 
 * 我们递归地将扫描子目录的任务委托给不同的线程.当扫描到一个文件时,线程不再返回一个计算结果,而是去更新一个AtomicLong类型的
 * 共享变量totalSize,AtomicLong提供了更改并取回一个简单long型变量的线程安全的方法.此外,我们还会用到另一个叫做pendingFileVisists的AtomicLong
 * 型变量,其作用是保存当前待访问文件的数量。当变量为0时,我们就调用countDown()来释放线程闩.
 * 
 * CountDownLatch最重要的方法是countDown()和await(),前者主要是倒数一次,后者是等待倒数到0,如果没有到达0,就只有阻塞等待了。
 * @author zeuskingzb
 *
 */
public class ConcurrentTotalFileSizeWLatch {
	private ExecutorService executorService;
	//保存当前待访问文件的数量
	final private AtomicLong pendingFileVisits = new AtomicLong();
	final private AtomicLong totalSize = new AtomicLong();
	final private CountDownLatch latch = new CountDownLatch(1);
	private void updateTotalSizeOfFilesInDir(final File file){
		long fileSize = 0;
		if (file.isFile()) {
			fileSize = file.length();
		}else{
			final File[] children = file.listFiles();
			if (children != null) {
				for (final File child : children) {
					if (child.isFile()) {
						fileSize +=child.length();
					}else{
						pendingFileVisits.incrementAndGet();
						System.out.println("pendingFileVisits.incrementAndGet()"+pendingFileVisits+"thread:"+Thread.currentThread().getName());
						executorService.execute(new Runnable() {
							@Override
							public void run() {
								// TODO Auto-generated method stub
								updateTotalSizeOfFilesInDir(child);
							}
						});
					}
					
					
				}
			}
			
		}
		totalSize.addAndGet(fileSize);
		System.out.println("pendingFileVisits.decrementAndGet()"+pendingFileVisits+"thread:"+Thread.currentThread().getName());
		if (pendingFileVisits.decrementAndGet() == 0) {
			//可以执行下一个动作
			latch.countDown();
		}
	}
	private long getTotalSizeOfFile(final String fileName) throws InterruptedException{
		executorService = Executors.newFixedThreadPool(100);
		pendingFileVisits.incrementAndGet();
		try{
			updateTotalSizeOfFilesInDir(new File(fileName));
			latch.await(100,TimeUnit.SECONDS);
			return totalSize.longValue();
		}finally{
			executorService.shutdown();
		}
		
	}
	public static void main(String[] args) throws InterruptedException {
		final long start = System.nanoTime();
		final long total = new ConcurrentTotalFileSizeWLatch().getTotalSizeOfFile("/usr");
		final long end = System.nanoTime();
		System.out.println("Total size :"+total);
		System.out.println("Time taken:"+ (end-start)/1.0e9);
	}
}


运行结果:

Total size :2912899381

Time taken:0.601758

分析:我们递归扫描子目录的任务委托给不同的线程。当扫描到一个文件时,线程不再返回一个计算结果,而是去更新一个AtomicLong类型的共享变量totalSize.AtomicLong 提供了更改并取回一个简单long型变量值的安全的方法。此外,我们还会用到另一个叫做pendingFileVisits的AtomicLong型变量,其作用是保存当前待访问文件(或子目录)的数量。当该变量为0时,我们就调用countDown()来释放纯程闩。



5.如果想要在线程间互发多组数据,则BlockingQueue接口可以派上用场.顾名思义,该接口的特点是:如果队列里没有可用空间,则插入操作将会被阻塞;而如果队列里没有可用数据,则删除操作将被阻塞。JDK提供了好几种功能各异的BlockingQueue。例如,若想要使插入操作将被阻塞。JDK提供了好几种功能各异的BlockingQueue.例如,若想要使插入操作和删除操作一一对应,可以使用SynchronousQueue类。该类的作用是将本线程的每一个插入操作与其他线程相应的删除操作相匹配,以完成类似于手递手形式的数据传输。而如果希望数据可以根据某种优先级在队列中上下浮动,则可以使用PriorityBlockingQueue.另外,如果只是想要一个简单的阻塞队列,我们可以选择用链表实现的LinkedBlockingQueue或数组方式实现的ArrayBlockingQueue.




package jvm;

import java.io.File;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 第五版
 * 这个版本的程序在性能 方面与前一版本相仿,但在代码简化方面又提升了一个档次,这主要归功于阻塞队列帮我们完成了线程间的数据
 * 交换和同步操作。
 * @author zeuskingzb
 *
 */
public class ConcurrentTotalFileSizeWQueue {
	private ExecutorService service;
	final private BlockingQueue<Long> fileSizes = new ArrayBlockingQueue<>(500);
	final AtomicLong pendingFileVisits = new AtomicLong();
	/**
	 * 多线程浏览文件
	 * @param file
	 */
	private void startExploreDir(final File file) {
		pendingFileVisits.incrementAndGet();
		//System.out.println("pendingFileVisits.incrementAndGet()"+pendingFileVisits);
		service.execute(new Runnable() {

			@Override
			public void run() {
				// TODO Auto-generated method stub
				exploreDir(file);
			}
		});
	}
	/**
	 * 浏览文件,文件的运行过程
	 * @param file
	 */
	private void exploreDir(File file) {
		long fileSize = 0;
		if (file.isFile()) {
			fileSize = file.length();
		} else {
			File[] children = file.listFiles();
			if (children != null) {
				for (final File child : children) {
					if (child.isFile()) {
						fileSize += child.length();
					} else {
						startExploreDir(child);
					}
				}
			}
			try {
				// 把fileSize加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续
				fileSizes.put(fileSize);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			pendingFileVisits.decrementAndGet();
			//System.out.println("pendingFileVisits.decrementAndGet()"+pendingFileVisits);
		}
	}
	private long getTotalSizeOfFile(String fileName) throws InterruptedException{
		service = Executors.newFixedThreadPool(100);
		try {
			startExploreDir(new File(fileName));
			long totalSize = 0;
			while (pendingFileVisits.get() >0 || fileSizes.size()>0) {
				// 从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据
				//否则知道时间超时还没有数据可取,返回失败。
				final long size = fileSizes.poll(10, TimeUnit.SECONDS);
				//System.out.println(size);
				totalSize += size;
			}
			return totalSize;
		}finally{
			service.shutdown();
		}
	}
	public static void main(String[] args) throws InterruptedException {
		final long start = System.nanoTime();
		final long total = new ConcurrentTotalFileSizeWQueue().getTotalSizeOfFile("/usr");
		final long end = System.nanoTime();
		System.out.println("Total size :"+total);
		System.out.println("Time taken:"+ (end-start)/1.0e9);
	}
}
结果:

Total size :2912899381

Time taken:0.594396

分析:我们为每个子目录的遍历工作都分配一个独立的任务 。每个任务负责计算给定目录下所有文件大小之和,并在计算结束之后调用阻塞函数put()将该值插入到队列当中.每个子目录的遍历都是在一个独立的线程中进行的,即线程之间互不影响。
主线程则首先启动目录遍历任务,随后便只需简单地循环读阻塞队列将其文件大小值累加起来,直至所有子目录遍历完成为止。




6.我们可以使用ExecutorService来管理线程并调度线程池中的线程来执行任务。然而线程池所含的线程数量是由程序员决定的,而程序员调度执行的任务和任务在执行过程中所创建的子任务之间也是没有任何区别的。所以为了提高效率和性能,Java7为我们带来了专门针对ExecutorService效率和性能的改进版的fork-join API
ForkJoinPool类可以根据可用的处理器数量和任务需求动态地对线程进行管理。Fork-join使用了work-stealing策略,即线程在完成自己的任务之后,发现其他线程还有活没有干完,就主动帮其他人一起干。该策略的使用不但提升了API的性能,而且还有助于提高线程利用率。
在Fork-join API  中,活动任务(active task) 所创建的子任务是由创建主任务所不同的另一套函数来负责调度的。通常我们在一个应用程序中只会使用一个fork-join池来调度任务,且由于该池使用了守护线程,所以用过之后也无需执行关闭操作。
为了更好地调度任务,我们提供了一些ForkJoinTask的实例(通常是其某个子类的实例)来配合ForkJoinPool函数的使用。我们可以用ForkJoinTask来创建(fork)任务,然后再将主线程join到任务的完成点上。ForkJoinTask有两个子类:RecursiveAction,RecursiveTask的子类则主要用于执行需要返回值的任务。

package jvm;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

/**
 * 第六版 用Fork-join api jdk 1.7
 * 
 *这里我们假定要遍历一个文件目录,因为文件的目录可以包含嵌套若干层的目录或者文件,从某种角度来说
 *构成了一个树形结构。我们再遍历到每个文件的时候,可以将目录作为一个子task来处理,这里就可以形成一个完整的
 *fork/join pool应用
 * 
 * 这个版本的的性能要比本章前面其他并发版本好很多。同时我们也注意要,对于大型的分层目录,程序也没有重蹈navie版本的覆辙
 * 
 * 在本例中,我们递归地将归扫描任务进行分解,直至任务无法再拆分,但一般来说,拆分粒度过细会显著增加线程调度的开销,所以我们并不希望将问题拆分得过小。
 * java.util.concurrent包中定义了很多线程安全的集合类,这些集合类即保证了并发编程环境下的数据安全性,同时也可以被当成同步点来使用。虽然线程安全是很重要的,
 * 但我们也不想为此牺牲太多性能。
 * @author zeuskingzb
 *
 */
public class FileSize {
	private final static ForkJoinPool forkjoinPool = new ForkJoinPool();
	private static class FileSizeFinder extends RecursiveTask<Long>{
		final File file;
		public FileSizeFinder(final File theFile) {
			// TODO Auto-generated constructor stub
			file = theFile;
		}
		
		@Override
		protected Long compute() {
			// TODO Auto-generated method stub
			long size = 0;
			if (file.isFile()) {
				size = file.length();
			}else{
				File[] children = file.listFiles();
				if (children != null) {
					List<ForkJoinTask<Long>> tasks = new ArrayList<ForkJoinTask<Long>>();
					for (File child : children) {
						if (child.isFile()) {
							size += child.length();
						}else{
							tasks.add(new FileSizeFinder(child));
						}
					}
					for (ForkJoinTask<Long> forkJoinTask : invokeAll(tasks)) {
						size +=  forkJoinTask.join();
					}
				}
			}
			return size;
		}
		
	}
	
	public static void main(String[] args) throws InterruptedException {
		final long start = System.nanoTime();
		final long total = forkjoinPool.invoke(new FileSizeFinder(new File("/usr")));
		final long end = System.nanoTime();
		System.out.println("Total size :"+total);
		System.out.println("Time taken:"+ (end-start)/1.0e9);
	}
}

结果:

Total size :2912899381

Time taken:0.498283


分析:Fork-join API通过working-stealing 完美地解决了这一问题.当一个任务处于等待其子任务结束的状态时,该任务的执行线程可以将该任务挂起,然后转去执行其他任务。
在FileSize类中,我们创建了一个ForkJoinPool实例的引用,该实例使用关键字static定义,即它可以在整个应用程序中共享.随后我们定义了一个名为FileSizeFinder的静态内部类.该类继承了RecursiveTask并实现了compute()函数,我们可以用它来作为任务的执行引擎。在compute()函数中.我们将给它目录下的所有文件大小累加起来,并将扫描和计算目录大小的工作委托给其他任务(即其他FileSizeFinder的实例)来完成。invokeAll()函数将等待所有子任务完成之后才会执行下一步循环累加操作。在任务被阻塞期间,其执行线程并非什么也不做一直傻等所有子任务结束(就像一个优秀的团队中那些有高度责任感的成员所做的那样),而是可以被调度去做其他任务。最后,每个任务都将在compute()函数结束时返回给定目录下所有文件和目录的大小!




通过以上的6种方式,带大家了解了 多线程的奥妙!我还会再后期发一些优秀的案例来和大家分享!