在学习任何东西之前,需要多反问自己,这个东西是什么(What)、有什么用(Where)、有什么优缺点(Why)、怎么用(How)。这样才能清楚学习的目的而不是盲目的跟风学习。
简述
作为线程池系列的第一篇文章,咱们先聊聊线程池是个什么东东。
线程的创建和销毁对于系统来说是一种开销,而使用线程池可以复用线程,减少线程的创建和销毁操作,提高系统的性能。另外一方面,线程池能够做到资源管理和限制。比方说线程池可以限制线程的数量,避免无限的创建线程导致OOM,在高并发的场景下尤为关键。
那使用线程池有什么好处?
- 重用存在的线程,减少线程对象创建、销毁的开销,提升性能。
- 充分利用有限的线程资源,同时避免过多资源竞争,避免堵塞。
- 减少线程调度的开销
- 提供定时执行、定期执行、单线程、并发数控制等功能。
emmm... 大概了解线程池是什么了,那线程池的使用场景有什么呢?
线程池的使用场景十分广泛,因为很少在实际工作中直接使用new Thread这种方式创建线程,一般都会使用线程池管理线程的创建和销毁。实际上,很多框架底层大量的使用线程池,如RocketMQ底层大量的使用定时执行的线程池,发送和接收各个服务的心跳信息等等
线程池真强大,那使用线程池正确的姿势是什么呢?
实际上,J.U.C包下已经贴心的为我们准备了几种线程池满足我们大部分的场景需求,而且我们也可以自定义线程池来满足我们项目实际场景需求。
Executors是线程池的工厂类,我们可以通过Executors工厂类快速的创建以下几种线程池:
- newFixedThreadPool():创建一个指定工作线程数量的线程池。主要被应用在线程资源有限,数据量较小或不可控场景,由于其线程数量有限,针对于过多的数据量,默认将会进行丢弃
- newCachedThreadPool():创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。主要被应用在响应时间要求高、数据量可控的场景,由于其不限制创建线程的个数,故若数据量不可控,会造成程序 OOM
- newSingleThreadExecutor():创建一个单线程的线程池。如果执行的线程异常退出会由其它线程取代
- newScheduledThreadPool():创建一个可以定时及周期性任务执行的线程池
- newSingleThreadScheduledExecutor():与newScheduledThreadPool相似,只不过是单线程
- newWorkStealingPool():创建一个具有抢占式操作的线程池
以上几种线程池的使用,咱们就可以当成一个黑盒子使用,需要的时候直接创建出来,但底层的原理万变不离其宗,因为以上的线程池对象都是创建ThreadPoolExecutor
初始化的(newWorkStealingPool线程池是通过ForkJoinPool),因此咱们学习线程池的重点应该是学习ThreadPoolExecutor类。
入门使用线程池
在了解ThreadPoolExecutor之前,咱们先从一个Demo开始入手吧!
public class ThreadPoolDemo {
private static final String filePath1 = "E:/JavaTest/test1.txt";
private static File file1 = new File(filePath1);
// 数据量
private static final int num = 2000 * 500;
public static void main(String[] args) {
/**
* 创建线程池
* coreSize:4
* maximumPoolSize:4
* keepAliveTime 和 TimeUnit.SECONDS:线程存活时间为1秒
* threadFactory:使用默认的线程工厂
* RejectedExecutionHandler:拒绝策略为AbortPolicy
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 4, 1, TimeUnit.SECONDS,
new ArrayBlockingQueue(20),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
// 创建10个任务提交到线程池
for (int i = 0; i < 10; i++) {
threadPoolExecutor.submit(() -> {
try {
// 调用io操作的方法,模拟线程执行的耗时,方便监控工具的查看
writeFile(file1);
} catch (IOException e) {
e.printStackTrace();
}
});
}
} finally {
threadPoolExecutor.shutdown();
}
}
// 写文件的IO操作
static void writeFile(File file) throws IOException {
// 判断是否有该文件
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
//创建输出缓冲流对象
BufferedWriter bufferedWriter = null;
try {
bufferedWriter = new BufferedWriter(new FileWriter(file));
for (int i = 0; i < num; i++) {
try {
bufferedWriter.write(i);
bufferedWriter.newLine();
bufferedWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " " + new Date() + " 执行完成");
}finally {
bufferedWriter.close();
}
}
}
运行结果:
pool-1-thread-1 Mon Apr 06 22:14:49 CST 2020 执行完成
pool-1-thread-2 Mon Apr 06 22:14:49 CST 2020 执行完成
pool-1-thread-3 Mon Apr 06 22:14:49 CST 2020 执行完成
pool-1-thread-4 Mon Apr 06 22:14:49 CST 2020 执行完成
pool-1-thread-3 Mon Apr 06 22:15:03 CST 2020 执行完成
pool-1-thread-1 Mon Apr 06 22:15:03 CST 2020 执行完成
pool-1-thread-2 Mon Apr 06 22:15:03 CST 2020 执行完成
pool-1-thread-4 Mon Apr 06 22:15:03 CST 2020 执行完成
pool-1-thread-1 Mon Apr 06 22:15:10 CST 2020 执行完成
pool-1-thread-3 Mon Apr 06 22:15:10 CST 2020 执行完成
使用Java自带的VisualVM监控工具查看线程的状况
总结:
结合输出结果和监控工具的视图,程序的确是创建了4个线程执行任务。例子中线程池相关的代码其实就只是调用submit
方法就可以很方便的指定创建多少线程执行任务。其实线程池的骚操作远远不止如此!接下来走进线程池源码,更加深入线程池是怎么创建线程,怎么管理线程,看下有什么设计思想值得学习。
下一篇文章:Java 线程池系列(下)之 ThreadPoolExecutor 源码剖析
如果觉得文章不错的话,麻烦点个赞哈,你的鼓励就是我的动力!对于文章有哪里不清楚或者有误的地方,欢迎在评论区留言~