前言
本来是准备直接写 CompletableFuture
线程池进阶文章的,但是总感觉不说一下线程池又不太好直接开展,所以本篇文章先讲解一下 Java 中的两种线程池。
为什么需要线程池
当我们需要异步处理任务时,最常用也是最简单的方式就是新开一个线程去做。而线程的创建和销毁是需要消耗 CPU 资源的,当异步任务越来越多时,如果一味的新开线程去处理,那么我们可能会无法控制 CPU 的资源。所以首先我们需要控制线程的开启数量。
类比数据库连接池,想一想之前使用 JDBC 访问数据库的时候是不是要先建立连接,也就是创建一个 Connection
对象。在 web 项目中,通常会有很多请求访问数据库,在使用框架 Spring+Mybatis
时,如果当前上下文没有事务的话那么每一个数据库操作方法都需要创建一个 Connection
,这样频繁的创建 Connection
对象无疑是资源的浪费,也会拉低接口吞吐量。
所以我们需要一个池子来维护数据库连接,也就是数据库连接池。同样我们也需要线程池。
线程池简介
线程池是一种基于池化思想管理线程的工具,类似于数据库连接池。
基于池化的思想是提前创建一定数量的 Connection
对象,然后将其存放起来,这样每次需要的时候直接从池中获取 Connection
对象即可,避免了频繁的创建和销毁的操作。线程池则是将线程 Thread
对象提前创建缓存起来,这样当提交任务的时候直接将任务提交给池中的线程即可,而不需要创建新的 Thread
。总结线程池的优势:
- 传统创建线程的方式对资源无限申请缺少抑制手段,容易引发资源耗尽的风险
- 通过复用线程池中的线程可以避免频繁创建线程,省去创建线程的时间,提高响应速度
- 线程池可以指定最大线程数量,超出的任务会在等待队列等待,不会出现大量线程耗尽服务器资源
传统线程池 ThreadPoolExecutor
ThreadPoolExecutor
是使用最为广泛的线程池实现类,查看类继承结构
顶层接口 Executor
提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供 Runnable
对象,将任务的运行逻辑提交到执行器 (Executor) 中,由 Executor 框架完成线程的调配和任务的执行部分。换句话说,你只需要把要做的事情给线程池即可。
使用 ThreadPoolExecutor
时直接实例化即可,它有很多重载的构造方法,这里我们选择参数最全的方法来进行讲解。
corePoolSize:核心线程数
用于执行任务的理想线程数,当线程池中正在运行的线程数量小于 corePoolSize
时,新任务来了会创建新线程执行任务
keepAliveTime 空闲线程存活时间
当线程数量大于 corePoolSize
时,超出的空闲线程在终止前等待新任务的最长时间。也就是说过了这个时间还没有新任务需要执行的话,空闲线程就会被销毁。很明显当 corePoolsize == maximumPoolSize
时, keepAliveTime
没有意义。
workQueue 工作队列
当线程池中正在运行的线程数量大于等于 corePoolSize
,新任务来了会放进队列等待。Java 提供的队列有 LinkedBlockingQueue、ArrayBlockingQueue、LinkedBlockingDeque
等,这里不详细介绍。
maximumPoolSize 最大线程数
线程池中最大允许的线程数量,很多人以为它的作用是这样的:当线程池中的任务超过 corePoolSize
时,会继续创建线程,直到线程数小于 maximumPoolSize
。这种理解是完全错误的