需求是这样的:最近在做一个有关学校考试的项目,其中老师可能任课好几门学科,每个学科有好几个班上,考完试后,老师想打印学生答题试卷(因为是上机考试),即涉及到了批量打印试卷,并且下载时将这些试卷打成一个压缩包,此功能用到了多线程
1.为什么用多线程
充分利用cpu资源
2.什么时候用多线程
- 高并发: 系统接受实现多用户多请求的高并发时,通过多线程来实现。
- 线程后台处理大任务: 一个程序是线性执行的。如果程序执行到要花大量时间处理的任务时,那主程序就得等待其执行完才能继续执行下面的。那用户就不得不等待它执行完。这时候可以开线程把花大量时间处理的任务放在线程处理,这样线程在后台处理时,主程序也可以继续执行下去,用户就不需要等待。线程执行完后执行回调函数。
- 大任务: 大任务处理起来比较耗时,这时候可以起到多个线程并行加快处理(例如:分片上传)。比如处理一个for循环时要花费大量时间,就可以考虑多线程了
3.多线程编码项目实战
先说一下此功能用到的知识点:
- 线程池
- 线程
- CountDownLatch
//1.CountDownLatch,计数
CountDownLatch doneSignal = new CountDownLatch(1000);
//2.创建线程池来存放线程,以防考生数量太多创建太多线程,占用过多资源
ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 150, 60000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>());
for (ExaminationModel examineeModel : notExamStudentInfo) {
。。。。。。
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
。。。。。。打印试卷的逻辑
} finally {
// 每执行完一个线程,数量减1
doneSignal.countDown();
}
}
});
//3. 将任务添加到线程池
executor.execute(thread);
}
// 4. 为了让所有的试卷都生成之后再执行压缩以及删除PDF文件,所以需要让主线程等待子线程执行完之后再执行await
try {
// 主线程等待
doneSignal.await();
System.out.println("线程运行时间:" + (System.currentTimeMillis() - startTime) + "ms");
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
//5. 关闭线程池(所有线程执行完关闭线程池)
executor.shutdown();
// 6. 将所有试卷打包zip
boolean flag = fileToZip();
实现思路:
创建线程池,和线程锁计数器,每打印一份试卷就是一个任务,将次任务添加到线程池中,然后线程执行,当所有的试卷都打印完,将所有的试卷打包成zip
知识点讲解:
- 之所以用线程池,是因为防止产生过多的线程,从而造成线程来回切换,而造成的形成损失
- CountDownLatch:它的作用是允许1或N个线程等待其他线程完成执行,在这用它是因为 要把所有的试卷,打包成zip压缩包,所以主线程要等所有的子线程即生成试卷要执行完才能打包,用到了CountDownLatch的await()和countDown()方法,有关CountDownLatch更多讲解,参见:https://www.cnblogs.com/skywang12345/p/3533887.html
- 我这创建的核心线程为50,最大线程为150,更具电脑cpu性能设置,执行程序打印线程号,线程号一直超不过50,是因为任务队列使用的是LinkedBlockingDeque,大小为Integer.MAX_VALUE(Integer.MAX_VALUE=7fffffff(十六进制) = 2147483647(十进制)),任务队列一直没有满,具体原因详情参见:https://mp.csdn.net/mdeditor/90082697#
4. 为什么不用join()
使用join()可能出现的问题:
- join()后面的代码可能提前完成,这样打包的数据就不全了
- join()过程中可能被打断了,这样系统就会抛异常(因为底层调用的是wait()),如果不想让程序判断就得做各种异常的判断,比较麻烦