一、线程池介绍
线程池是一种多线程的管理机制,它使得程序员能够避免繁琐的线程管理工作。这里所说的线程管理工作主要是以下这些:
- 线程的生命周期管理(从创建到销毁)
- 并发控制(主要是不同场景下的并发数量控制)
- 任务调度(任务进入线程池的方式)
- 异常处理
而线程池主要就是围绕以上这几个方面为用户提前做了高效的实现。那下面先大概介绍一下线程池的核心变量、工作流程和Java对于线程池的实现方式。
1.1.线程池的核心变量
线程池的核心变量包括:
- 核心线程数(CorePoolSize):线程池中始终保持活动的最小线程数量,即使这些线程处于空闲状态也不会被回收(除非设置了允许核心线程超时)。
- 最大线程数(MaximumPoolSize):线程池允许创建的最大线程数量。当任务队列已满且当前线程数 < 最大大小时,线程池会创建新线程处理任务。
- 任务队列(WorkQueue):当所有线程都在运行时,任务队列用于存放待执行的任务。
- 线程存活时间(KeepAliveTime):非核心线程(即超出核心大小的线程)在空闲状态下的存活时间。超过该时间后,空闲线程会被终止。
- 拒绝策略(RejectedExecutionHandler):当线程池和队列都满时,处理新提交的任务的方法。
1.2.线程池的工作流程
1.3.Java的线程池实现方式
这里以ThreadPoolExecutor的实现为例:
在了解了线程池的基本概念以后,下面开始总结书里的内容。
二、在任务与任务执行策略之间的隐性耦合
首先介绍一下什么是任务执行策略,说白了就是任务在线程池中是如何被安排执行的,这个策略有几个核心的内容:
- 线程数量控制:核心线程数量、最大线程数量
- 线程回收:包括
keepAliveTime
- 拒绝策略:
- 带扩展的钩子方法:主要是beforeExecute、afterExecute和terminate三个方法
在Java中,Executor的框架可以将任务的提交与任务的执行策略解耦开来(无论是什么样的线程池实现,都利用submit或者execute提交策略执行),但是书中提到了几种特殊的任务类型,这几种类型的任务是与特定的线程池实现有比较紧密耦合关系的,如果匹配出现问题会产生很多的问题。
2.1.依赖性任务
如果并发的任务存在相互依赖关系,那么当线程池的最大线程数量较小时,可能会造成依赖任务等待被依赖任务的执行结果,而被依赖任务在队列中等待执行,从而造成线程饥饿死锁的问题。所以对于有依赖性任务执行需求的线程池,需要有意识的扩大线程池内线程数量,并且在配置文件中记录线程池的大小限制或者配置限制。
2.2.使用线程封闭机制的任务
如果所执行的任务有线程封闭执行要求的,往往会凭借单线程对于并发安全的承诺,在其他线程安全机制的设置中存在缺陷(比如为考虑线程安全,将任务执行所需的对象封闭在任务线程中,而未设置其他线程安全保障的),一旦通过多线程的线程池来执行,会造成线程安全问题。
2.3.对响应时间敏感的任务
比如GUI应用程序,用户对于点击按钮时的等待时间的容忍度是很低的,因此对于这些任务需要尽量减少任务在队列中等待的时间,可以通过扩大线程池以及对长时间任务设置超时限制进行优化。
2.4.使用ThreadLocal的任务
ThreadLocal即线程的本地变量,如果线程有设置本地变量的,只有当本地变量的生命周期受限于任务整体的生命周期时,这个本地变量才有意义,因为一旦任务执行完成,空闲的线程就有被清除的风险。因此对于该类任务要注意线程生命周期的设置。
三、设置线程池的大小
首先,线程池的大小是其任务执行策略的核心要素,它需要很好的平衡好线程池的吞吐量与内存的使用量。这里有一个计算方式,可以大致估算一个线程池的最优线程数(基本的思路是任务可以分为I/O密集型任务和计算密集型任务,如果一个线程在执行I/O工作的时间,刚好其余线程完成了计算任务,那这样的CPU利用效率是最高的):
假设有N个CPU、W为I/O任务的平均等待时间、C为计算密集型任务的等待时间、U为CPU的利用率,n为最优线程个数,那么:
则有:
四、配置ThreadPoolExecutor
4.1.标准的ThreadPool实现
Executors中有工厂方法可以返回一些标准的ThreadPool,他们有着标准的任务执行策略:
newCachedThreadPool(弹性线程池):
- 线程数量:无上限(
Integer.MAX_VALUE
),空闲线程60秒后自动销毁。 - 任务队列:使用
SynchronousQueue
(无容量,直接移交任务给线程)。 - 适用场景:大量短生命周期的异步任务(如HTTP请求、瞬时计算)。
newFixedThreadPool(固定线程池):
- 线程数量:固定(用户指定),核心线程永不回收(即使空闲)。
- 任务队列:无界
LinkedBlockingQueue
(可能导致OOM)。 - 适用场景:长期稳定负载的任务(如后台批处理、可控的并发请求)。
newScheduledThreadPool(定时线程池):
- 线程数量:固定(用户指定),核心线程永不回收。
- 任务队列:延迟队列
DelayedWorkQueue
(按执行时间排序)。 - 适用场景:支持定时执行、周期性执行(如轮询、心跳检测)。
4.2.自定义ThreadPoolExecutor
如果标准的ThreadPool无法满足要求,可以通过ThreadPoolExecutor的构造函数来实例化自定义的ThreadPoolExecutor对象,构造函数的参数基本就是前面提到的线程池的核心变量:
- int corePoolSize
- int maximumPoolSize
- long keepAliveTime
- TimeUnit unit
- BlockingQueue<Runnable> workQueue
- ThreadFactory threadFactory
- RejectedExecutionHandler handler
这里核心线程数、最大线程数、线程存活时间等变量前面已经介绍过了,这里重点介绍一下任务队列(workQueue)、线程工厂方法(threadFactory)以及拒绝策略(handler)
4.2.1.任务队列
任务队列用于临时存储待执行的任务,当线程池中的线程都在忙碌时,新提交的任务会进入队列等待。任务队列的选择直接影响线程池的吞吐量、资源消耗和任务调度行为。
在ThreadPoolExecutor的构建中,任务队列是一个阻塞队列(BlockingQueue),在具体实现时可以有如下实现:
队列类型 | 特点 | 适用场景 |
---|---|---|
LinkedBlockingQueue |
无界队列:默认 Integer.MAX_VALUE ,任务无限堆积,可能导致内存溢出。 |
已知任务量可控,且内存充足时使用。 |
ArrayBlockingQueue |
有界队列:需指定容量,队列满时触发线程扩容或拒绝策略。 | 需要限制内存占用的高并发场景。 |
SynchronousQueue |
无缓冲队列,任务直接交给线程执行(无存储能力),依赖 maximumPoolSize 。 |
高并发短任务,要求低延迟。 |
PriorityBlockingQueue |
优先级队列(任务需实现 Comparable ),按优先级调度任务。 |
VIP 任务优先处理。 |
DelayedWorkQueue |
延迟队列(用于 ScheduledThreadPoolExecutor ),按执行时间排序。 |
定时任务、周期性任务。 |
4.2.2.线程工厂方法
线程工厂方法是线程池构造线程的方法,默认只有一个newThread方法,默认的线程工厂方法会创建一个新的、非守护线程,并且不包含特殊配置信息。所以如果我们需要定制线程池所创建线程的配置,则需要定制线程工厂方法(比如为创建的线程指定一个UncaughtExceptionHandler,或者配置日志写入等)。
4.2.3.拒绝策略
前文提到了当线程池的任务队列已满且线程数达到 maximumPoolSize
时,新提交的任务会触发拒绝策略。拒绝策略决定了如何应对无法处理的任务,是线程池健壮性的关键机制。Java提供了4种内置的拒绝策略:
策略 | 行为 | 适用场景 |
---|---|---|
AbortPolicy (默认) |
直接抛出 RejectedExecutionException 异常。 |
需要明确感知任务被拒绝的场景。 |
CallerRunsPolicy |
由提交任务的线程自己执行该任务(相当于回退到同步调用)。 | 保证任务不丢失,但可能阻塞提交线程。 |
DiscardPolicy |
静默丢弃新提交的任务,不抛异常也不执行。 | 允许丢弃非关键任务(如日志记录)。 |
DiscardOldestPolicy |
丢弃队列中最旧的任务(即队列头部的任务),然后重试提交新任务。 | 优先处理新任务,但可能丢失旧任务。 |
文中还提到,如果没有预定义的饱和策略,还可以通过使用Semaphore信号量来控制任务的到达率,使得任务队列中的任务数量维持在一个恒定的数量之下。
五、扩展ThreadPoolExecutor
ThreadPoolExecutor提供了三个扩展方法(beforeExecute、afterExecute、terminated),用于扩展其在其生命周期前后的行为,包括添加日志、计时、监视、统计信息收集或者释放资源等功能,特别的对于afterExecute,无论任务是正常返回还是抛出异常放回,afterExecute都会被调用。