线程池核心参数有什么,拒绝策略有什么
线程池的核心参数主要定义了线程池如何管理线程、任务以及在资源不足时的行为。在Java的java.util.concurrent.ThreadPoolExecutor
中,这些核心参数体现在其构造函数中:
-
corePoolSize
(核心线程数):- 线程池中保持存活的核心线程数量,即使它们处于空闲状态,也不会被销毁(除非设置了
allowCoreThreadTimeOut
)。 - 当提交一个新任务到线程池时,如果当前运行的线程数少于
corePoolSize
,即使其他工作线程处于空闲状态,线程池也会创建一个新线程来处理该任务。
- 线程池中保持存活的核心线程数量,即使它们处于空闲状态,也不会被销毁(除非设置了
-
maximumPoolSize
(最大线程数):- 线程池允许创建的最大线程数量。
- 当工作队列已满,并且当前运行的线程数小于
maximumPoolSize
时,线程池会创建新的非核心线程来处理任务。 - 如果
corePoolSize
等于maximumPoolSize
,则创建的是一个固定大小的线程池。
-
keepAliveTime
(线程空闲存活时间):- 当线程池中的线程数量大于
corePoolSize
时,如果一个非核心线程的空闲时间超过keepAliveTime
,这个线程就会被终止,以回收资源。 - 这个参数只有当线程数大于
corePoolSize
时才有效。 - 如果设置了
allowCoreThreadTimeOut(true)
,那么核心线程在空闲时间超过keepAliveTime
后也会被终止。
- 当线程池中的线程数量大于
-
unit
(时间单位):keepAliveTime
参数的时间单位,例如TimeUnit.SECONDS
,TimeUnit.MILLISECONDS
等。
-
workQueue
(工作队列 / 任务队列):- 用于保存在核心线程都在忙,且无法立即处理的新提交任务的队列。它是一个
BlockingQueue
接口的实现。 - 常见的队列类型有:
ArrayBlockingQueue
:基于数组的有界阻塞队列,按FIFO(先进先出)原则排序。LinkedBlockingQueue
:基于链表的阻塞队列,按FIFO排序。容量可以是有界的,也可以是无界的(默认情况下容量为Integer.MAX_VALUE
,近似无界)。使用无界队列时,maximumPoolSize
参数可能不会生效,因为任务会一直入队。SynchronousQueue
:一个不存储元素的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作,反之亦然。通常用于需要直接将任务从生产者交给消费者的场景,或者配合一个非常大的maximumPoolSize
使用(如CachedThreadPool
)。PriorityBlockingQueue
:一个支持优先级排序的无界阻塞队列。
- 队列的选择对线程池的行为有很大影响。
- 用于保存在核心线程都在忙,且无法立即处理的新提交任务的队列。它是一个
-
threadFactory
(线程工厂):- 用于创建新线程的工厂。可以自定义线程的名称、是否为守护线程、优先级等。
- 默认情况下,线程池使用
Executors.defaultThreadFactory()
,创建的线程具有相同的优先级和非守护状态,线程名为"pool-N-thread-M"。
-
handler
(拒绝策略 / 饱和策略 - RejectedExecutionHandler):- 当工作队列已满,并且线程池中的线程数也达到了
maximumPoolSize
时,线程池会采取指定的策略来处理无法接收的新任务。
- 当工作队列已满,并且线程池中的线程数也达到了
线程池的拒绝策略 (RejectedExecutionHandler) 主要有以下几种Java中预定义的实现:
-
AbortPolicy
(中止策略):- 这是
ThreadPoolExecutor
默认的拒绝策略。 - 当新任务无法提交时,会抛出
RejectedExecutionException
运行时异常,调用者可以捕获这个异常并进行处理。 - 这种策略会直接中断任务的提交过程。
- 这是
-
CallerRunsPolicy
(调用者运行策略):- 当新任务无法提交时,该任务不会被丢弃,也不会抛出异常。而是由提交任务的那个线程(调用
execute
方法的线程)自己来执行这个任务。 - 这种策略提供了一种简单的反馈控制机制,可以减慢新任务的提交速度,因为提交者自己忙于执行任务时,就无法快速提交更多任务。
- 当新任务无法提交时,该任务不会被丢弃,也不会抛出异常。而是由提交任务的那个线程(调用
-
DiscardPolicy
(丢弃策略):- 当新任务无法提交时,会默默地丢弃该任务,不抛出异常,也不做任何其他处理。
- 如果允许任务丢失,这是一种简单的策略。
-
DiscardOldestPolicy
(丢弃最旧策略):- 当新任务无法提交时,会丢弃工作队列队头(即等待时间最长)的任务,然后尝试重新提交当前这个新任务。
- 如果工作队列是优先级队列,那么丢弃的是优先级最低的任务。
- 这种策略也可能会丢失任务。
除了这四种预定义的策略,开发者还可以通过实现RejectedExecutionHandler
接口来定义自己的拒绝策略,以满足特定的业务需求。例如,可以将任务持久化到磁盘,或者记录日志后丢弃,或者等待一段时间后重试等。
拓展延申:
-
线程池状态:
ThreadPoolExecutor
内部维护了线程池的状态,主要有:RUNNING
: 能够接受新任务,并处理已添加的任务。SHUTDOWN
: 不接受新任务,但会处理已添加的任务。调用shutdown()
方法会进入此状态。STOP
: 不接受新任务,不处理已添加的任务,并中断正在处理的任务。调用shutdownNow()
方法会进入此状态。TIDYING
: 所有任务都已终止,workerCount
为0,线程池会进入该状态并准备调用terminated()
钩子方法。TERMINATED
:terminated()
方法已完成。
-
线程池的预热和动态调整:
- 预热:可以在线程池启动时,通过调用
prestartAllCoreThreads()
或prestartCoreThread()
来预先创建核心线程,避免在接收到初始任务时才开始创建线程的延迟。 - 动态调整:
ThreadPoolExecutor
提供了setCorePoolSize()
,setMaximumPoolSize()
,setKeepAliveTime()
等方法,允许在运行时动态调整线程池的参数。这可以用于根据系统负载情况自适应地调整线程池配置,但需要谨慎操作,避免不当调整导致性能问题或资源浪费。
- 预热:可以在线程池启动时,通过调用
-
监控线程池:
ThreadPoolExecutor
提供了很多方法用于监控其状态,如:getPoolSize()
: 当前线程池中的线程数量。getActiveCount()
: 当前正在执行任务的线程数量。getCompletedTaskCount()
: 已完成的任务数量。getTaskCount()
: 曾计划执行的任务总数(包括已完成、正在执行和队列中等待的)。getQueue().size()
: 当前工作队列中的任务数量。 这些信息对于监控线程池的健康状况、诊断问题以及调优参数非常有价值。许多APM工具也会集成对线程池的监控。
-
合理配置线程池大小: 这是一个常见且重要的问题,没有万能公式,需要根据任务类型(CPU密集型 vs. I/O密集型)、任务处理时间、系统资源(CPU核心数、内存)、期望的吞吐量和响应时间等因素综合考虑。
- CPU密集型任务:线程数通常建议设置为CPU核心数或核心数+1,以减少上下文切换。
- I/O密集型任务:由于线程在等待I/O时会阻塞,可以配置更多的线程,例如
CPU核心数 * (1 + 平均等待时间 / 平均计算时间)
。一个经验公式是CPU核心数 / (1 - 阻塞系数)
,阻塞系数在0到1之间。 实际配置往往需要通过压力测试和性能监控来调整和优化。