1、前言
我们知道多线程的使用,是为了最大限度发挥现代多核处理器的计算能力,提高系统的吞吐量和性能。但是如果不加以控制和管理,随意使用多线程,对系统性能反而会有不利的影响。线程数量和系统CPU资源是息息相关的,随意使用甚至可能会耗尽系统CPU资源和内存资源。
2、池化技术
为了应对多线程管理和控制的问题,引入池化技术。所谓池化技术,顾名思义就是造一个池子,讲需要管理的东西交给池子管理,而用完之后就放回池子。就像小孩子的玩具收纳箱,玩玩具的时候从收纳箱中拿出玩具,玩完之后一定要教小孩子将玩具放回玩具收纳箱中。池化技术通过优化资源的分配效率,从来达到性能的调优。
其实在Java编程中,池化技术不仅仅在多线程中使用这种方式,其他地方也同样用到了池化技术。如:数据连接池,对象池,内存池等等。
3、为什么要用线程池
前面基础多少讲到了为什么使用线程池的原因。这里详细说明以下几个原因,由ChatGPT来总结一下:
在多线程编程中,频繁地创建和销毁线程是一项昂贵的操作。因此,使用线程池来管理线程的创建、复用和销毁是一种有效的方式。 以下是几个原因解释为什么要使用线程池:
- 降低资源消耗:线程的创建和销毁需要消耗系统资源,如内存和CPU。使用线程池可以重用线程,避免频繁创建和销毁线程,从而降低了资源消耗。
- 提高系统响应性:线程池能够提高系统的并发能力和响应性。通过合理地配置线程池的大小,可以同时执行多个任务,提高系统的吞吐量和响应时间。
- 任务调度和线程复用:线程池可以管理和调度任务的执行。它维护一组线程,可以根据任务的到达顺序和优先级来选择合适的线程执行任务,避免任务争抢和冲突。同时,线程池中的线程可以被重复利用来执行多个任务,避免了频繁创建线程的开销。
- 控制并发线程数量:通过设置线程池的大小和任务队列的容量,可以限制并发执行的线程数量,防止系统资源被过度占用,从而提高系统的稳定性和可靠性。
- 简化线程编程:使用线程池可以将任务的提交和执行解耦,简化了线程编程的复杂性。开发人员只需关注任务的实现和提交,无需手动创建和管理线程,从而降低了出错的概率。
3.1、线程池优点
线程池优点很明显,上面提到为什么要使用线程池的几个原因就是对应的优点,这里不赘述。
3.2、线程池缺点
线程池的缺点也很明显:
- 资源占用:线程池本身会占用一定的系统资源,包括内存和CPU。如果线程池的大小设置不合理,可能会导致资源浪费或不足的问题。
- 线程泄露:如果没有正确地关闭线程池,或者任务执行过程中出现异常导致线程无法正常释放,可能会导致线程泄露,进而影响系统性能。
- 需要合理配置:线程池的性能和效果受到配置参数的影响,需要根据具体场景合理配置线程池的大小、任务队列的容量等参数,否则可能会影响系统的性能和响应性。
- 难以处理长时间任务:线程池主要适用于短时间的任务处理,如果任务执行时间过长,可能会导致线程池中的线程被长时间占用,影响其他任务的执行。
4、如何使用线程池
最简单的线程池使用方法:
public class ThreadPoolTest {
public static void main(String[] args) {
// 创建一个固定大小的线程池,大小为3
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交任务给线程池执行
for (int i = 0; i < 10; i++) {
// 执行提交任务
executorService.execute(() -> {
// ......
});
}
// 关闭线程池
executorService.shutdown();
}
}
- 通过Executors.newFixedThreadPool(3)方法创建了一个线程池,该线程池固定线程数量为3;
- 使用executorService.execute()方法执行向线程池内提交的线程任务;
- 执行完后,通过executorService.shutdown();关闭线程池资源;
通过简单的线程池使用方式,我们就完成了基本的线程池操作。线程池会自动管理线程的创建和销毁,以及任务的调度和执行,帮我们简化了多线程编程的复杂性。
5、JUC线程池
5.1、Executor
Executor 线程池顶级接口,类似一个线程池工厂。接口中只有一个execute()方法,接收Runnable类型。注意这里返回值类型是void。
5.2、ExecutorService
ExecutorService继承自Executor接口,添加了关闭线程池以及等待中断等方法。同时添加了submit来提交线程任务,除了接收Runnable以外,还可以接收Callable类型,也增加了返回值。
5.3、AbstractExecutorService
AbstractExecutorService是实现ExecutorService接口的抽象类。默认实现了个别如submit方法等。
5.4、ScheduledExecutorService
该类是为了实现带有定时器功能的线程池。ScheduledExecutorService也是一个接口。包含了定时和延迟处理的方法。
5.5、ThreadPoolExecutor方法参数
ThreadPoolExecutor重点看这个类。ThreadPoolExecutor是JUC中提供的默认线程池实现类。提供了丰富的配置选项和线程池管理功能。
提供了4个可选配置的构造函数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
我们重点关注其中的几个参数:corePoolSize,maximumPoolSize,keepAliveTime,workQueue,threadFactory,handler。
5.5.1、corePoolSize
核心线程数。指线程池中始终保持的线程数量,就算他们处于空闲状态,也不会被销毁。而一直存活的最小线程数量。
5.5.2、maximumPoolSize
最大线程数。指线程池中允许的最大线程数量。当maximumPoolSize的数量大于corePoolSize时,多的那部分空闲状态下的线程,会再超过一定时间后被销毁,只保留corePoolSize的核心线程数。
5.5.3、keepAliveTime
非核心线程保持空闲的时间。如果超过这个时间,多的那部分空闲状态下的线程就会被销毁。可以通过unit设置时间单位。
5.5.4、BlockingQueue
任务队列,阻塞队列。当前并发执行的线程数与系统资源有关。当你设置了大于当前系统可负载的线程数量时,多的那部分自然要进行等待,从而进入等待队列。
当线程池中线程数量达到corePoolSize时,且都处于运行状态,这时候后续提交的线程任务会进入到缓存阻塞队列中,等待执行。这个缓存阻塞队列也就是workQueue。
JUC中提供的BlockingQueue有以下几种:
- ArrayBlockingQueue:由数组实现的有界阻塞队列。需要指定队列的容量大小。当队列已满时,添加任务的操作将被阻塞,直到队列中有空位。ArrayBlockingQueue适用于固定大小的线程池,可以控制线程池中的最大任务数。
- LinkedBlockingQueue:由链表实现的可选有界或无界阻塞队列。如果创建LinkedBlockingQueue时没有指定容量大小,那么它将是一个无界队列,可以无限制地添加任务。如果指定了容量大小,它将成为一个有界队列。当队列已满时,添加任务的操作将被阻塞。LinkedBlockingQueue适用于任务数比较大且变化较大的场景。
- SynchronousQueue:一个没有缓冲区的阻塞队列。每个插入操作必须等待一个相应的删除操作,反之亦然。SynchronousQueue适用于任务直接交付给线程执行的场景,可以有效地避免任务的排队和缓冲。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。元素按照比较器或元素的自然顺序进行排序。PriorityBlockingQueue适用于需要按照优先级顺序处理任务的场景。
注:当使用了无界队列后,maximumPoolSize会失效。
这些BlockingQueue的区别主要在于容量限制、阻塞特性和元素排序。根据具体的需求和场景,选择合适的BlockingQueue可以提高线程池的性能和效率。
5.5.5、threadFactory
线程工厂。用于创建线程的工厂类。可以通过设置线程工厂来自定义线程的创建方式,例如设置线程名称、线程优先级等
5.5.6、RejectedExecutionHandler
拒绝策略。用于处理无法接收的任务。当线程池已满且任务无法提交时,会触发拒绝策略来处理这些任务。
JUC提供的RejectedExecutionHandler有以下几种:
- AbortPolicy(默认策略):该策略会直接抛出RejectedExecutionException异常,阻止任务的提交。
- CallerRunsPolicy:当线程池无法接收任务时,会将任务返回给调用者执行。也就是说,由提交任务的线程来执行该任务。这样可以降低任务提交速度,但可能会影响调用线程的性能。
- DiscardPolicy:该策略会默默丢弃无法接收的任务,没有任何提示和异常。这可能导致任务的丢失,潜在的风险需要注意。
- DiscardOldestPolicy:当线程池无法接收任务时,会丢弃队列中最旧的任务,然后尝试再次提交任务。这样可以保留较新的任务,但可能会丢失一些较旧的任务。
这些拒绝策略在处理无法接收的任务时具有不同的行为,可以根据具体的需求和业务场景选择合适的策略。需要根据任务的重要性、丢失任务的风险以及业务需求来综合考虑选择合适的拒绝策略。
5.6、手动创建一个线程池
private final static ThreadPoolExecutor threadPoolExecutor;
static {
// 这里利用hutool提供的ThreadFactoryBuilder,创建一个线程池工厂,并配置线程名称前缀
// ThreadFactory是个接口,也可以自定义实现
ThreadFactory threadFactory = ThreadFactoryBuilder.create().setNamePrefix("common-thread-pool-").build();
threadPoolExecutor = new ThreadPoolExecutor(
// 核心线程数为7,
// 通常IO密集型的可以配置为2 * cpu数量
// CPU密集型的可以配置为 cpu数量 + 1
7,
// 最大线程数量20个
20,
// 空闲等待时间,1分钟
// 超过1分钟,多余的空闲线程会被销毁
1 * 60,
// 空闲等待时间,单位
TimeUnit.SECONDS,
// 有界等待队列,固定长度为50
// 如果使用无界队列,需要考虑内存占用问题
new ArrayBlockingQueue(50),
// 线程工厂
threadFactory,
// 拒绝策略
new ThreadPoolExecutor.AbortPolicy());
}
6、小结
到这里,基本交代了线程池的一些基础概念,以及关于线程池的一些基础使用。后面的章节会讲到线程池的几个实现类,以及简单的场景使用案例。
持续更新中......