Java 多线程编程 线程池(Thread Pool)模式

Thread Pool 模式简介

为什么要使用线程池模式

Thread Pool 模式的核心思想

Thread Pool 模式的本质

Thread Pool 实现类

Thread Pool 模式的架构

Thread Pool 模式的主要参与者

客户端代码向线程池提交任务序列图

线程池执行任务的序列图

Thread Pool 模式的评价与现实考量

工作队列的选择

线程池大小调校

线程池监控

线程泄漏

可靠性与线程池饱和处理策略

死锁

线程池空闲线程清理

Thread Pool 模式的可复用实现代码

Java 标准库实例


Thread Pool 模式简介

为什么要使用线程池模式

因为线程在执行任务时需要消耗 CPU 时间和内存等资源,线程对象(Thread 实例)本身以及线程所需的调用栈(Call Stack) 也占用内存,并且 Java 中创建一个线程往往意味着 JVM 会创建相应的依赖于宿主机操作系统的本地线程(Native Thread ),所以复用一定的线程就成为了一种解决该问题的做法,故线程池模式应运而生。

Thread Pool 模式的核心思想

使用队列对待处理的任务进行缓存,并复用一定数量的工作者线程去取队列中的任务进行执行。

Thread Pool 模式的本质

使用极其有限的资源去处理相对无限的任务。

Thread Pool 实现类

JDK 1.5 引入的标准库类: java.util.concurrent.ThreadPoolExecutor 

Thread Pool 模式的架构

Thread Pool 模式的主要参与者

       

  • Thread Pool:负责接收和存储任务以及工作者线程的生命周期管理。其主要方法及职责如下:
    • submit:用于接收一个任务,客户端代码调用该方法向线程池提交一个任务;
    • shutdown:关闭线程池对外提供的服务。
  • Promise:可借以获取相应的任务执行结果的凭据对象。其主要方法及职责如下:
    • getResult:获取相应任务的执行结果;
    • setResult:设置相应任务的执行结果。
  • WorkQueue:工作队列,实现任务的缓存。其主要方法及职责如下:
    • enqueue:将任务存入队列;
    • dequeue:从队列中取出一个任务。
  • WorkerThread:负责任务执行的工作者线程。其主要方法及职责如下:
    • run:逐一从工作队列中取出任务执行;
    • runTask:执行指定的任务。

客户端代码向线程池提交任务序列图

第 1 步:客户端代码调用 ThreadPool 参与者实例的 submit 方法提交一个任务;

第 2、3 步:submit 方法调用 WorkQueue 参与者实例的 enqueue 方法将任务缓存;

第 4、5 步:submit 方法创建与当前任务相应的 Promise 参与者实例,并将其作为返回值返回。

线程池执行任务的序列图

第 1 步:某个工作者线程开始运行,其 run 方法被 JVM 调用;

第2 ~ 4步:这几个步骤是 run 方法中的循环。该循环重复地从工作队列中取出一个任务执行。若工作队列中暂时没有任务,则当前工作者线程会被挂起,直到工作队列中有新的任务;

第 5 步:其他工作线程者开始运行,相应线程的 run 方法被 JVM调用;

第6 ~ 8步:类似于上述第 2 ~ 4 步,其他工作者线程取工作队列中的任务执行。

Thread Pool 模式的评价与现实考量

Thread Pool 模式用过复用一定数量的工作者线程去执行不断被提交的任务,节约了线程这种有限而昂贵的资源。

Thread Pool 模式还可以带来以下好处:

  • 抵消线程创建的开销,提高响应性;
  • 封装了工作者线程生命周期管理;
  • 减少销毁线程的开销。

工作队列的选择

  • 有界队列(Bounded Queue):可以通过 ArrayBlockingQueue 、有界的 LinkedBlockingQueue 实现。以有界队列作为工作队列可以限定线程池中待执行任务的数量,这在一定程度上可以限制资源的消耗。适合在提交给线程池执行的各个任务之间是互相独立(而非有依赖关系)的情况下使用;
  • 无界队列(Unbound Queue):可以通过 LinkedBlockingQueue 实现。适合在任务占用的内存以及其他稀缺资源比较少的情况下使用;
  • 直接交接队列(SynchronousQueue):是一个特殊的 BlockingQueue 。SynchronousQueue 没有容量,每一个插入操作都要等待一个相应的删除操作,反之每一个删除操作都要等待对应的插入操作。如果使用 SynchronousQueue  ,提交的任务不会被真实的保存,而总是将新任务提交给线程工作者执行,如果没有空闲的进程,则尝试创建新的进程,如果线程已经达到最大值,则执行拒绝策略。 使用 SynchronousQueue 作为工作队列,工作队列本身并不限制待执行的任务的数量。但此时需要限定线程池的最大大小为一个合理的有限值,而不是 Integer.MAX_VALUE,否则可能导致线程池中的工作者线程的数量一致增加到系统资源所无法承受为止。

线程池大小调校

概念:线程池大小指线程池中的工作者线程的数量。

依据:合理的线程池大小取决于该线程池所要处理的任务的特性、系统资源状况以及任务所使用的稀缺资源状况的因素。

  • 系统资源:系统资源主要考虑系统 CPU 个数以及 JVM 堆内存的大小。在 Java 中我们可以调用 java.lang.Runtime 类的 availableProcessors 方法获取 JVM 宿主机 CPU 个数;
  • 任务的特性:主要考虑任务是 CPU 密集型、 I/O 密集型,还是混合型(同时包含较多计算和 I/O 操作)。
    • CPU 密集型:S = ^{_{N}}cpu + 1
    • I/O 密集型:S = 2 * ^{_{N}} cpu
    • 混合型:S = ^{_{N}} cpu * ^{_{U}}cpu * (1 + \tfrac{WT}{ST})
      • S : 线程池的合理大小;
      • _{N}cpu:CPU 个数;
      • _{U}cpu:目标 CPU 使用率;
      • WT:任务执行线程进行等待的时间;
      • ST:任务执行线程使用 CPU 进行计算的时间。
  • 任务所使用的稀缺资源:如数据库连接。

线程池监控

ThreadPoolExecutor 类提供了对线程池进行监控的相关方法:

方法 用途

getPoolSize()

获取当前线程池大小
getQueue() 返回工作队列实例,通过该实例可获取工作队列的当前大小
getLargestPoolSize() 获取工作者线程数曾达到的最大数,该数值有助于确认线程池的最大大小设置是否合理
getActiveCount() 获取线程池中当前正在执行任务的工作者线程数(近似值)
getTaskCount() 获取线程池到目前为止所接受到的任务数(近似值)
getCompletedTaskCount() 获取线程池到目前为止已经处理完毕的任务数(近似值)

线程泄漏

线程泄漏(Thread Leak)是指线程池的工作者线程意外中止,使得线程池中实际可用的工作者线程变少。

线程泄漏通常是由于线程对象的 run 方法中异常处理没有捕获 RuntimeExecption 和 Error  导致 run 方法意外返回,使得相应线程提前中止。

另外一种可以事实上造成线程泄漏的场景:如果线程池中的某个工作者线程执行的任务设计外部资源等待,如等待网络 I/O,而该任务又没有对这种等待指定时间限制。那么外部资源如果一直没有返回该任务所等待的结果,就会导致执行该任务的工作者线程一直处于等待状态而无法执行其他任务,这就形成了事实上的线程泄漏。

可靠性与线程池饱和处理策略

如果我们在创建 ThreadPoolExecutor 实例的时候指定了有界队列作为工作队列,那么当线程池中的工作队列满,并且工作者线程数量已达到最大工作者线程数(线程池的最大大小)时,线程池就处于饱和状态。此时,新提交给线程池的任务就会被拒绝。

ThreadPoolExecutor 提供了线程池饱和处理策略的接口和一些预定义的实现类:

实现类 所实现的处理策略
ThreadPoolExecutor.AbortPolicy 直接抛出异常
ThreadPoolExecutor.DiscardPolicy 丢弃当前被拒绝的任务(二部抛出任何一场)
ThreadPoolExecutor.DiscardOldestPolicy 将缓冲区中最老的任务丢弃,然后重新尝试接纳被拒绝的任务
ThreadPoolExecutor.CallerRunsPolicy 在客户端线程中执行被拒绝的任务

死锁

如果线程池中执行的任务在其执行过程中又会想同一个线程池提交另外一个任务,而前一个任务的执行结束又依赖于后一个任务的执行结果,那么当线程池中所有的线程都处于这种等待其他任务的处理结果,而这些线程所等待的任务仍然还在工作队列中的时候,由于线程池已经没有可以对工作队列中的任务进行处理的工作者线程,这种等待就会一直持续下去而形成死锁(DeadLock)。

因此,适合提交给同一线程池实例执行的任务是相互独立的任务,而不是彼此有依赖关系的任务。

要执行彼此有依赖关系的任务可以考虑将不同类型的任务交给不同的线程池实例执行,或者对负责任务执行的线程池实例进行如下配置:

  1. 设置线程池的最大大小为一个有限值,而不是默认值 Integer.MAX_VALUE;
  2. 使用 SynchronousQueue 作为工作队列;
  3. 使用 ThreadPoolExecutor.CallerRunsPolicy 作为线程池饱和处理策略。

线程池空闲线程清理

线程池中长期处于空闲状态(即没有在执行任务)的工作者线程会浪费宝贵的线程资源。ThreadPoolExecutor 支持将其核心工作者线程以外的空闲线程进行清理。创建 ThreadPoolExecutor 实例时,我们可以在其构造器的第3、4个参数中指定一个空闲持续时间。核心工作者线程以外的工作者线程空闲了指定时间以后, ThreadPoolExecutor 就可以将其清理掉。

Thread Pool 模式的可复用实现代码

利用 ThreadPoolExecutor 实现 Thread Pool 模式,应用代码只需要完成以下几件事情:

  1. 【必需】创建一个 ThreadPoolExecutor 实例。根据应用程序的需要,创建 ThreadPoolExecutor 实例时指定一个合适的线程池饱和处理策略;
  2. 【必需】创建 Runnable 实例用于表示待执行的任务,并调用 ThreadPoolExecutor 实例的 submit 方法提交任务;
  3. 【必需】使用 ThreadPoolExecutor 实例 的submit 方法返回值获取相应任务的执行结果。

Java 标准库实例

Java Swing 中类 javax.swing.SwingWorker 可用于执行耗时较长的任务。该类使用了 Thread Pool 模式。SwingWorker 内部维护了一个线程池(ThreadPoolExecutor 实例),该线程池包含了若干个工作者线程用于执行提交给 SwingWorker 的任务。

                                                                                                                       以上资料来自于《Java 多线程编程实战指南(设计模式篇)》


猜你喜欢

转载自blog.csdn.net/IbelieveSmile/article/details/81457240