Java并发编程实战 学习笔记--第8章 线程池的使用

发布于:2025-05-20 ⋅ 阅读:(16) ⋅ 点赞:(0)

一、线程池介绍

线程池是一种多线程的管理机制,它使得程序员能够避免繁琐的线程管理工作。这里所说的线程管理工作主要是以下这些:

  • 线程的生命周期管理(从创建到销毁)
  • 并发控制(主要是不同场景下的并发数量控制)
  • 任务调度(任务进入线程池的方式)
  • 异常处理

而线程池主要就是围绕以上这几个方面为用户提前做了高效的实现。那下面先大概介绍一下线程池的核心变量、工作流程和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为最优线程个数,那么:

\frac{nC}{NU} = W+C

则有:

n = NU(1+\frac{W}{C})

四、配置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都会被调用。


网站公告

今日签到

点亮在社区的每一天
去签到