文章目录
- 一、为什么要用线程池
- 1.1 单线程的问题
- 1.2 手动创建多线程的问题
- 1.3 线程池的作用(优点)
- 1.4 线程池的使用场景
- 二、线程池的基础知识
- 2.1 线程池的核心组件
- 2.2 JUC中的线程池架构
- 2.3 线程池的配置参数
- 2.4 线程池常见的拒绝策略(可自定义)
- 2.5 线程池的生命周期
- 2.6 线程池的任务调度规则
- 2.7 线程池执行器的钩子方法
- 三、使用基础
- 3.1 常见的工作队列
- 3.2 Executors提供的线程池
- 3.3 使用Executors创建线程的问题
- 四、开发实践
- 4.1 线程数量配置
- 4.2 提交任务:以submit(callable)为例
- 4.3 关闭线程池
- 参考
一、为什么要用线程池
1.1 单线程的问题
单线程无法利用CPU的多核资源,且存在阻塞问题。使用单个线程来处理一组任务,若当前任务阻塞了线程,该线程将失去CPU的使用权,而不是去执行其他可以执行的任务。
1.2 手动创建多线程的问题
需要开发人员去管理线程的生命周期,麻烦,且可能出错。
还存在并发数不可控的问题,如每次到达一个任务时都新建一个线程来处理,当大量任务到达时,会创建大量的线程,导致线程间的竞争加剧,且可能导致系统资源耗尽。
1.3 线程池的作用(优点)
- 节省线程创建和销毁的开销:线程池通过复用一组线程,可以节省频繁创建和销毁线程的开销。
- 控制并发数:线程池可以控制最大并发数,避免线程过多导致系统资源耗尽。
- 提高响应速度:当任务到达时,可以直接使用线程池中已经创建好的线程,节省了新建线程的时间。
- 管理线程:线程池负责内部线程的生命周期管理,包括创建、销毁和调度等,无需人工干预。
1.4 线程池的使用场景
- 处理并发请求:每个请求都要一个线程来处理,使用线程池来复用一组线程,能节省创建和销毁线程的开销。
- 执行异步任务:避免耗时操作(如邮件发送、文件上传)阻塞主线程,可以使用线程池来异步执行这些任务。
- 处理IO密集型任务:线程在进行IO操作时会阻塞,使用线程池来调度这些任务,可以在任务阻塞时调度其他任务继续执行,避免CPU空闲。
- 处理计算密集型任务:使用线程池来执行计算密集型任务,可以充分利用CPU的多核资源,也能控制并发数量避免系统资源耗尽。
- 后台服务:利用线程池来执行后台服务,与前台线程的执行独立,互不影响。
- 执行定时任务与周期性任务:可以使用
ScheduledThreadPool
在后台执行这些任务,与前台线程的执行独立。
二、线程池的基础知识
2.1 线程池的核心组件
- 线程池管理器:负责线程池的创建、销毁、管理和配置。
- 工作线程:工作线程是线程池中的实际工作者,负责执行提交给线程池的任务。
- 任务接口:每个提交到线程池的任务都需要实现一个任务接口。
Runnable
用于定义无需返回值的任务,而Callable
则用于定义可以有返回值的任务,并且可以抛出异常。 - 工作队列(任务队列):用于暂存等待被执行的任务,有多种实现方式,未必是先进先出。
- 线程工厂:线程工厂的作用是定义一个规范来创建线程,允许在创建线程时进行定制化设置,例如设置线程的名称、优先级、是否为守护线程(daemon thread)等属性。
- 拒绝策略:见后。
(后三个核心组件是线程池的后三个配置参数)
2.2 JUC中的线程池架构
Executor
是线程池管理器的顶层接口,只定义了一个方法void execute(Runnable command)
,用来提交Runnable
任务。
ExecutorService
该接口扩展了Executor
,增加了更多管理线程池的方法,如提交Callable
任务、提交批量任务、线程池的生命周期管理(shutdown()
和shutdownNow()
)、获取线程池状态等。
AbstractExecutorService
实现了该接口中的部分方法,为创建自定义的线程池服务实现提供了基础框架。
ThreadPoolExecutor
继承自AbstractExecutorService
,是JUC中线程池管理器的核心实现类。
Executors
是一个静态工厂类,提供了多种线程池管理器。
ScheduledExecutorService
该接口扩展了ExecutorService
,增加了定时执行和周期性执行任务的功能。ScheduledThreadPoolExecutor
实现该接口,专为计划任务而设计。
2.3 线程池的配置参数
- corePoolSize:核心线程数。
- maximumPoolSize:最大线程数。
- keepAliveTime:空闲线程存活时间。当线程池中的工作线程数量超过核心线程数时,额外的工作线程在空闲这个时间段后会被终止,直到工作线程数量降到核心线程数。但如果设置了
allowCoreThreadTimeOut
为true
,那么核心线程也可以被终止。 - unit:空闲线程存活时间的单位,可以用枚举类
TimeUnit
设置。 - workQueue:工作队列。
- threadFactory:线程工厂。
- handler:拒绝策略。当阻塞队列已满且工作线程数大于等于最大线程数时,无法接受新的任务,会按照拒绝策略进行处理。
2.4 线程池常见的拒绝策略(可自定义)
- AbortPolicy:拒绝策略。是默认策略,会将新任务拒绝,并且抛出
RejectedExecutionException
异常。 - DiscardPolicy:抛弃策略。会将新任务丢掉,但不会跑出异常。
- DiscardOldestPolicy:抛弃最老任务策略。不是丢弃新任务,而是先丢弃最先进入队列的任务,然后将新任务入队。
- CallerRunsPolicy:调用者执行策略。让提交任务的线程去执行新任务,而不是使用线程池中的线程去执行。
2.5 线程池的生命周期
- New:新建状态。当线程池创建后,但尚未开始执行任务时,它处于新建状态。此时,线程池中的线程尚未启动。
- Running:运行状态。新建的线程池开始接受并处理任务后就会进入运行状态,这是线程池的正常工作状态,可以接收新任务和处理工作队列中的任务。
- Shutdown:关闭状态。调用
shutdown()
方法后,线程池会进入关闭状态。此时,线程池不再接受新任务,但不会立刻终止,它会继续处理队列中已有的任务直到所有任务完成。 - Stop:停止状态。调用
shutdownNow()
方法后,线程池会进入停止状态,此时会中断正在执行任务的线程,清空任务队列,并返回未开始执行的任务列表。 - Tidying:整理状态。完成关闭状态或停止状态的工作后,线程池会进入整理状态。此阶段,线程池中的线程数量降为0,即将调用
terminated()
钩子方法。 - Terminated:终止状态。在整理状态后,线程池调用
terminated()
方法并进入终止状态。此时线程池生命周期结束,会释放所有资源。
2.6 线程池的任务调度规则
当向线程池提交一个任务时:
- 若线程池中的工作线程数量小于核心线程数,执行器总是优先创建一个新的工作线程来执行任务,而不是使用一个空闲的工作线程(目的是快速让线程池中有足够的活跃线程)。若工作线程数量大于等于核心线程数,则根据工作队列的情况进行相应的处理。
- 若工作队列未满,则将任务入队。若工作队列已满,则根据工作线程数量与最大线程数的关系进行相应的处理。
- 若工作线程数大于等于最大线程数,则执行拒绝策略。否则,会新建一个非核心线程来立即执行新的任务。
2.7 线程池执行器的钩子方法
beforeExecute(Thread t, Runnable r)
: 任务执行之前的钩子方法。
afterExecute(Runnable r, Throwable t)
: 任务执行之后的钩子方法。
terminated()
: 线程池终止时的钩子方法。
三、使用基础
3.1 常见的工作队列
工作队列基于阻塞队列实现,我们可以实现BlockingQueue
来自定义阻塞队列,也可以使用JUC中提供的阻塞队列:
- ArrayBlockingQueue:基于数组的有界阻塞队列。队列大小是固定的,在创建时必须指定。由于是基于数组,所以访问速度快,但可能有扩容受限的问题。
- LinkedBlockingQueue:基于链表的阻塞队列。如果不指定容量,则默认为Integer.MAX_VALUE。相比ArrayBlockingQueue,插入和删除操作可能稍微慢一点,但由于链表的动态性,它可以更灵活地调整大小。
- PriorityBlockingQueue:基于最小堆的无界的优先级队列,元素按照自然排序或提供的比较器进行排序。任务按照优先级顺序被处理,而非先进先出。
- DelayQueue:基于
PriorityBlockingQueue
的无界阻塞队列,其中元素只有在延迟期满后才能被获取,否则将阻塞等待,常用于实现定时任务和延迟操作。 - SynchronousQueue:一个特殊的队列,它没有内部容量,每个插入操作必须等待另一个线程的对应移除操作,反之亦然。适用于直接的生产者-消费者传递,非常适合传递性操作。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,支持公平和非公平模式。它还提供了一个
tryTransfer()
方法,允许生产者直接将元素转移给等待中的消费者,如果没有消费者等待,则可以选择是否阻塞(生产者可阻塞)。 - LinkedBlockingDeque:双向链表实现的双向阻塞队列,既可以用作栈,也可以用作队列。它支持有限或无界的队列,并提供了
putFirst()
、putLast()
等方法来控制元素的插入位置。
3.2 Executors提供的线程池
3.3 使用Executors创建线程的问题
- 定长线程池/单线程话线程池:阻塞队列无界,可能导致JVM出现OOM(Out Of Memory)异常。
- 定时线程池/可缓存线程池:最大线程数量不设限上,如果任务提交较多,就会造成大量的线程被启动,可能造成OOM异常,也可能导致CPU线程资源耗尽。
四、开发实践
4.1 线程数量配置
IO密集型
核心线程数设置得比CPU核心数稍大,因为当线程在等待I/O时,CPU可以调度其他线程执行。最大线程数可以设置得更高,甚至远大于CPU核心数,具体数值取决于系统的I/O能力及预期的并发水平。一般推荐设置为2 * CPU核心数或更高,但需注意不要设置得过高以免过度消耗系统资源。
计算密集型
核心线程数和最大线程数通常设置为CPU核心数,因为在这种情况下,更多的线程并不能带来性能提升,反而会因为上下文切换带来额外开销。
4.2 提交任务:以submit(callable)为例
// 创建线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 创建Callable任务对象
Callable<Result> task = () -> {
// ...
};
// 提交任务并获取Future对象
Future<Result> future = executor.submit(task); // 提交任务并获取Future
// 获取结果
try {
// get()方法会阻塞,如果任务执行过程中发生了异常,那么此处会抛出该异常
Result res = future.get();
} catch (InterruptedException e) {
// 处理中断异常...
} catch (ExecutionException e) {
// 处理执行异常...
}
// 关闭ExecutorService
executor.shutdown();
4.3 关闭线程池
调用shutdown()
或shutdownNow()
后,当前线程不会等待线程池的关闭,若当前线程结束了,可能导致内存泄露和资源浪费。
若线程池中还有活动的线程,就算主线程结束了,JVM也会因为存在活跃的非守护线程而无法退出,导致应用程序无法正常退出。
awaitTermination()
方法用来阻塞当前线程,直到线程池中的所有任务完成执行或者超时,或者当前线程被中断。可以用于等待线程池中的线程全部执行完后再退出当前线程,线程池中的所有线程在超时前执行完毕,会返回true并恢复当前线程的执行。
ExecutorService executorService = Executors.newFixedThreadPool(10);
// ... 提交任务到线程池
// 关闭线程池
executorService.shutdown();
try {
// 等待一定时间,直到超时或者线程池中的线程全部执行结束
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
// 如果超时,则尝试强制关闭
executorService.shutdownNow();
// 等待一定时间,直到超时或者线程池中的线程全部执行结束
if (!executorService.awaitTermination(60, TimeUnit.SECONDS))
System.err.println("线程池未在规定时间内停止!");
}
} catch (InterruptedException ie) {
// 强制终止线程池
executorService.shutdownNow();
// 保持当前线程的中断状态
Thread.currentThread().interrupt();
}
参考
Java线程池(超详细)
Java 多线程:彻底搞懂线程池