5.使用线程池
(1)单线程版本
上面是一次文件校验,假如说我们需要同时进行100次文件校验,那么就需要执行100次文件校验方法吗?这样显然是不可取的,我们首先改造一下文件校验方法,把主方法也实现Runnable方法,从而可以并行执行。
先改造单线程版本的,把原来的文件校验方法写到run里面。
private String oldPath;
private String newPath;
private String outPath;
public FileCheckSingleThread(String oldPath, String newPath, String outPath) {
this.oldPath = oldPath;
this.newPath = newPath;
this.outPath = outPath;
}
@Override
public void run() {}
先测试一下,跑个100次,先搞个循环:下面这种写法看似是多线程,实际上并没有并发执行,因为创建了线程之后就调用了join方法,只有这个线程执行完毕后主线程才会继续创建下一个线程:
startTime = System.currentTimeMillis(); //获取开始时间
for (int i = 0; i < 100; i++) {
Thread fileCheckSingleThread = new Thread(new FileCheckSingleThread(oldPath, newPath, outPath));
fileCheckSingleThread.start();
fileCheckSingleThread.join();
}
endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("100次文件校验完成 耗时:" + (endTime - startTime) + "ms");
测试一下也可以看到,总耗时就是单个线程执行时间的100倍
单线程校核完成 耗时:1178ms
单线程校核完成 耗时:1187ms
单线程校核完成 耗时:1331ms
单线程校核完成 耗时:1181ms
单线程校核完成 耗时:1188ms
100次文件校验完成 耗时:123772ms
那么应该怎么写呢?一种很傻的方法就是不用循环手写100个线程。。。当然是开玩笑的,可以用CountDownLatch 来实现。
CountDownLatch是java.util.concurrent包中一个类,CountDownLatch主要提供的机制是多个(具体数量等于初始化CountDownLatch时count的值)线程都达到了预期状态或者完成了预期工作时触发事件,其他线程可以等待这个事件来触发自己后续的工作。等待的线程可以是多个,即CountDownLatch可以唤醒多个等待的线程。到达自己预期状态的线程会调用CountDownLatch的countDown方法,而等待的线程会调用CountDownLatch的await方法。
具体方法是在FileCheckSingleThread中增加一个属性,每个线程执行完毕时去调用countDown方法。
private CountDownLatch end;
long endTime = System.currentTimeMillis(); //获取结束时间
System.out.println("单线程校核完成 耗时:" + (endTime - startTime) + "ms");
end.countDown();
而在主线程中需要创建一个CountDownLatch对象,调用其await方法。
final CountDownLatch end = new CountDownLatch(100);
startTime = System.currentTimeMillis(); //获取开始时间
for (int i = 0; i < 100; i++) {
Thread fileCheckSingleThread = new Thread(new FileCheckSingleThread(oldPath, newPath, outPath,end));
fileCheckSingleThread.start();
}
end.await();
endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("100次文件校验完成 耗时:" + (endTime - startTime) + "ms");
测试一下:
单线程校核完成 耗时:38232ms
单线程校核完成 耗时:38227ms
单线程校核完成 耗时:38221ms
单线程校核完成 耗时:38249ms
单线程校核完成 耗时:38290ms
单线程校核完成 耗时:38308ms
100次文件校验完成 耗时:39164ms
可以看到执行时间明显降低了,但是缺点也很明显,所有的100个线程同时在执行,如果这个数字更大一点呢?机器可能会爆掉,而且每次都要去创建新线程也很麻烦。
因此,就需要用到线程池去管理了。关于线程池详见我的另一篇博客https://blog.csdn.net/m0_37657841/article/details/88822003
为了避免系统频繁地创建和销毁线程,我们可以让创建的线程复用。在使用线程池后,创建线程变成了从线程池中获得空闲线程,关闭线程变成了向线程池中归还线程(类似数据库连接池)。
先设置一个线程数为10的定长线程池,
startTime = System.currentTimeMillis(); //获取开始时间
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executorService.submit(new FileCheckSingleThread(oldPath, newPath, outPath));
}
executorService.shutdown();
while (true) {
if (executorService.isTerminated()) {
endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("100次文件校验完成 耗时:" + (endTime - startTime) + "ms");
break;
}
}
可以看到在总时长几乎相同的情况下,每个线程的运行时间明显降低了
单线程校核完成 耗时:4367ms
单线程校核完成 耗时:4029ms
单线程校核完成 耗时:3343ms
单线程校核完成 耗时:3276ms
单线程校核完成 耗时:3147ms
单线程校核完成 耗时:3082ms
100次文件校验完成 耗时:39398ms
(2)改造多线程版本
创建一个新类FileCheckThreadPool,直接按照单线程版本的改法,测试一下
看到性能反而下降了?这是为啥呢?
修改文件分析完成 耗时:3495ms
多线程校核完成 耗时:3790ms
修改文件分析完成 耗时:3549ms
多线程校核完成 耗时:3588ms
修改文件分析完成 耗时:2369ms
多线程校核完成 耗时:3059ms
修改文件分析完成 耗时:1898ms
多线程校核完成 耗时:2463ms
100次文件校验完成 耗时:51250ms
猜想可能是因为每个校验文件的线程在执行时都会再创建三个子线程去分别对应校验新增,修改和删除文件。因为cpu资源总共就那么多,创建了过多的线程反而会造成性能损失。
那怎么办呢?先把三个子线程也用线程池管理起来,然后尝试把用CountDownLatch把每个校验线程里面的三个子线程在文件校验主线程之前执行。
结果发现了死锁,想一想也能想到,因为线程池总共只有10个位置,文件校验线程放进去之后,就一直在等待子线程执行,结果子线程在等待队列中卡住了,把线程池设置为400个,发现就能顺利往下进行,果然是死锁了。
那要怎么办呢?设置一个线程的优先级?加入的时候让文件校验的子线程优先执行。
写了半天没写出来,有些多线程方面的知识没有学到,感觉有点复杂,就先放在这里吧。。。以后再更新