本人使用的是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
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)
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()来释放纯程闩。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()将该值插入到队列当中.每个子目录的遍历都是在一个独立的线程中进行的,即线程之间互不影响。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