2023- 深入理解Java 线程池:线程池参数调优与技巧
深入理解Java 线程池:线程池参数调优与技巧
在现代编程中,线程池已经成为了不可或缺的一部分。
线程池简介
线程池概述
线程池是一种管理和重用线程资源的机制。通常情况下,每个任务都需要一个独立的线程来执行。当任务变得越来越多,每个任务都创建一个新线程就会导致系统负荷过重。这时候线程池的使用就能很好地解决这个问题。
线程池维护了一组空闲线程,当有任务需要执行时,就从线程池中选择一个空闲的线程执行任务,当任务执行完成后,这个线程就会被重新放回线程池,供下一次任务使用,这样可以节省线程创建和销毁的时间成本,提高系统的执行效率和响应速度。
线程池API 介绍
java.util.concurrent
包下,主要有以下几个类:
Executor
:线程池顶层接口,定义了线程池的execute() 方法,提交任务到线程池去执行。ExecutorService
:线程池的具体实现接口,通过submit() 方法向线程池提交任务,返回Future ,可以通过Future 获得任务的执行情况。ScheduledExecutorService
:实现类似于Timer 的计划任务调度功能。ThreadPoolExecutor
:是ExecutorService 的一个实现类,也是Java 中的线程池实现的核心实现类。通常情况下,我们是使用ThreadPoolExecutor 来实现线程池功能。
这些
FixedThreadPool - 它包含固定数量的线程启动,线程数设定的越多并发就越高,内部的任务队列FIFO( 先进先出) 。CachedThreadPool - 这个池默认规定的池大小( 线程数) 是没有限制的,比较适合执行线程简小且IO 密集型或计算密集型task ,避免等待I/O 浪费时间(wait) 来提供更大的利用率调用超过线程数的task 到thread 中。SingleThreadExecutor - SingleThreadExecutor 是指有个线程池保持单线程工作。它用于背后处理活动在一个job queue 中不大并且涉及器不借此启动新线程,避免上下文切换(set Context_Switches) 的操作开销。ScheduledThreadPool - 允许用户执行时间的安排或周期性任务在future 类型值上的结果。以可调类型提供相对 延迟( 取代sleep) 和fixed-delay 并符合UTC 执行时间的时间单元作为其参数(一些days 、hours、 minutes、seconds 等等)
线程池的用法
- 创建线程池执行器:可以通过
Executor 框架提供的工厂方法或通过ThreadPoolExecutor 构造函数传入参数的方式来进行创建。 - 提交任务:通过使用
submit() 方法将多个任务提交到线程池中等待执行。 - 关闭线程池:可以手动关闭线程池、也可以使用
shutdown() 或shutdownNow() 方法关闭线程池。注意关闭线程池需要注意已经提交到线程池中的任务是否全部完成。
线程池常用参数含义
corePoolSize ( 核心线程池大小)
该参数指定核心线程池中线程的数量。当提交一个新任务时,如果当前线程池中的线程数少于
- maximumPoolSize(最大线程池大小)
该参数指定总线程池大小,包括核心线程池和非核心线程池。在任务队列满了的情况下,可以创建的最大线程数。如果此时运行的线程数已经等于了
- keepAliveTime(线程存活时间)
当线程池中的数量大于
- workQueue(任务队列)
任务队列是存储被提交但尚未被执行的任务的阻塞队列。常用的任务队列有如下几类:
- ArrayBlockingQueue:基于数组的有限队列,可以指定容量。
- LinkedBlockingQueue:基于链表的无限队列,可以无限扩展。
- PriorityBlockingQueue:优先级队列,可以自定义排列顺序。
- SynchronousQueue:同步队列,不存储数据,只在提交和取出数据时传递数据。
- RejectedExecutionHandler(拒绝策略)
拒绝策略是当任务队列满了需要执行拒绝策略来处理新提交的任务。提供了几种预定义的拒绝策略:
- AbortPolicy:直接抛出异常,默认策略。
- CallerRunsPolicy:主线程执行该任务。
- DiscardOldestPolicy:丢弃队列中最老的任务,然后重新尝试执行当前任务。
- DiscardPolicy:默默丢弃提交的任务,没有异常。
线程池调优
线程池的合理性评估
在开始调整线程池之前,需要对当前的系统进行全面的评估。可以通过使用系统监控软件,来确定系统的各项指标如
如何选择合适的参数值
- 核心线程池大小
核心线程池大小应该根据需要处理的并发任务数以及
- 最大线程池大小
最大线程池大小应该根据系统应对的最高并发数来确定。如果最大线程池大小比较大,会导致系统资源的浪费;如果比较小,会导致请求被拒绝。建议根据硬件资源、负载、并发量等实际情况来确定。
- 存活时间
存活时间通常设置为
- 队列选择
常用的阻塞队列有
- 如果任务数目大于队列长度,那么将会创建新线程去处理任务。因此队列应该尽量设置为有限阻塞队列,避免无限制的任务添加。
- 若系统负载较大或并发量较大时,可以使用
LinkedBlockingQueue ,因为LinkedBlockingQueue 可以无限制存储任务,防止任务丢失。 - 若任务量较小,建议使用
ArrayBlockingQueue ,因为存储数组有限,有利于反馈当前任务的处理情况。
- 拒绝策略
拒绝策略有四种:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy。其中,
线程池及线程池大小调优技巧
- 核心线程池数量调优
核心线程池数量不宜过多,因为每个线程都需要占用内存和
- 最大线程池数量调优
最大线程池数量一般设置:最大线程池数量
- 存活时间调优
设定一个适当的线程存活时间,可以有效地减少线程的创建和销毁带来的性能开销。在存活时间到达之后,多余的线程会被回收,从而释放系统资源。
- 队列调优
任务队列是存储被提交但尚未被执行的任务的阻塞队列。在选择队列类型时,应考虑任务数量和任务类型,以及需要处理的并发请求数。常用的阻塞队列有
- 拒绝策略调优
拒绝策略通常分为四种:AbortPolicy、CallerRunsPolicy、
应用场景
CPU 密集型应用线程池设置:
在
IO 密集型应用线程池设置:
在
- 其它应用场景:
对于各种不同的场景,应该根据实际情况进行参数的设置。比如生产者
总结
- 线程池参数设置的思考
线程池对于提高系统性能和可管理性非常重要,线程池的性能和效率很大程度上取决于参数的设置。对于不同的业务和场景,需要根据实际情况来设置线程池的参数。
- 核心线程池大小和最大线程池大小应该根据实际需求来设置,一般建议把核心线程池大小设置成
CPU 核心数加1 ,而最大线程池大小可以设置为CPU 核心数乘2 。 - 阻塞队列的大小和类型应该根据实际情况来设置,不同类型的阻塞队列对于不同的场景都有其优缺点,比如
ArrayBlockingQueue 适用于任务执行比较平缓的场景,而LinkedBlockingQueue 则适用于任务执行频繁的场景。 - 线程存活时间应该根据实际情况来设置,一般建议设置为
60s 左右。 - 拒绝策略应该根据实际情况来设置,比如当线程池中的队列已满或达到了最大线程数时,应该使用合适的拒绝策略。
- 注意事项:
- 线程池中的线程数量不是越多越好,过多的线程数量会增加线程上下文切换的开销,同时也会占用过多的系统资源。
- 线程池的大小应该根据实际情况来设置,不同的场景和业务具有不同的特点,需要根据实际情况做出合理的选择。
- 阻塞队列的大小和类型应该根据实际情况做出选择,为了防止任务丢失,应该将其设置成有界队列,同时也需要考虑队列的容量大小和类型。
- 线程池的拒绝策略应该根据实际情况来选择,不同的业务场景具有不同的特点,需要做出合理的选择。