前言

原则上来讲,Java 的线程既是工作单元,也可以是执行机制。从 JDK1.5 开始,工作单元与执行机制可以被分离开来。其中,执行机制由 Executor 框架提供,而工作单元包括 Runnable 和 Callable(比较常规的两种声明线程的方法,可以通过传给 Thread 的构造函数来创建线程,并使用 Thread 实例的 start() 方法来启动线程,这种创建线程的方式,就是将工作单元与执行机制绑定在一起了)。

 

Executor 框架

Executor框架主要由 3 大部分组成如下:

  • 任务:包括被执行任务需要实现的接口,Runnable 接口或 Callable 接口
  • 任务的执行:包括任务执行机制的核心接口 Executor,以及继承自 Executor 的 ExecutorService 接口。Executor 框架有两个关键类实现了 ExecutorService 接口(ThreadPoolExecutor 和 ScheduledThreadPoolExecutor)
  • 异步计算的结果:包括接口 Future 和实现 Future 接口的 FutureTask 类

本文主要介绍 Executor 框架中最核心的类:ThreadPoolExecutor ,它是线程池的实现类。

 

ThreadPoolExecutor 详解

ThreadPoolExecutor 最基本的构造函数如下所示:
Java 线程池详解(二):三个现成的线程池详解
它需要 7 个参数,这些参数,我在上一篇文章中详细的介绍过,这里不再赘述了,链接如下:

Java 线程池详解(一):线程池实现原理及使用

J.U.C 包其实给我们提供了一种非常方便的创建线程池的方式,那就是使用工厂类 Executors 来创建。Executors 可以创建 4 种类型的线程池(ThreadPoolExecutor ),分别是:

  • FixedThreadPool
  • SingleThreadExecutor
  • CachedThreadPool
  • ScheduledThreadPool

需要明确的是,前三种线程池都是基于 ThreadPoolExecutor 的,它们都是由 ThreadPoolExecutor 的构造函数实例化出来的,只不过传入的参数不同而已。(严格意义上来讲其实 ScheduledThreadPool 也是基于 ThreadPoolExecutor 的)

创建方式如下所示:
Java 线程池详解(二):三个现成的线程池详解第四种不是本文重点,接下来主要看看前三种

 

FixedThreadPool 详解

FixedThreadPool 被称为可重用固定线程数的线程池。下面是 FixedThreadPool 的源代码实现。

Java 线程池详解(二):三个现成的线程池详解

FixedThreadPool 的 corePoolSize 和 maximumPoolSize 都被设置为创建 FixedThreadPool 时指定的参数 nThreads。

当线程池中的线程数大于 corePoolSize 时,keepAliveTime 为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止。这里把 keepAliveTime 设置为 0L,意味着多余的空闲线程会被立即终止(但其实对于 FixedThreadPool 来说,根本不会有超过核心线程数量的多余线程存在)。

FixedThreadPool 的 execute() 方法的运行示意图如下图所示

Java 线程池详解(二):三个现成的线程池详解
对上图的说明如下:

(1)如果当前运行的线程数少于 corePoolSize,则创建新线程来执行任务。

(2)在线程池完成预热之后(当前运行的线程数等于 corePoolSize ),将任务加入 LinkedBlockingQueue。

(3)线程执行完(1)中的任务后,会在循环中反复从 LinkedBlockingQueue 获取任务来执行。

FixedThreadPool 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Integer.MAX_VALUE)。使用无界队列作为工作队列会对线程池带来如下影响。

(1)当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize

(2)由于(1),使用无界队列时 maximumPoolSize 将是一个无效参数

(3)由于(1)和(2),使用无界队列时 keepAliveTime 将是一个无效参数

(4)由于使用无界队列,运行中的 FixedThreadPool(未执行方法 shutdown() 或 shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution() 方法)

FixedThreadPool 的适用场景: 为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。

 

SingleThreadExecutor 详解

SingleThreadExecutor 是使用单个 worker 线程(线程池中的线程都被包装成 Worker)的 Executor。

下面是 SingleThreadExecutor 的源代码实现。(SingleThreadExecutor 可以理解为就是传入 nThreads 为 1 的 FixedThreadPool)
Java 线程池详解(二):三个现成的线程池详解
SingleThreadExecutor 的 corePoolSize 和 maximumPoolSize 被设置为 1。其他参数与 FixedThreadPool 相同。

SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。

SingleThreadExecutor 使用无界队列作为工作队列对线程池带来的影响与 FixedThreadPool 相同,这里就不赘述了.

SingleThreadExecutor的 execute() 方法的运行示意图如下图所示:

Java 线程池详解(二):三个现成的线程池详解
对上图的说明如下:

(1)如果当前运行的线程数少于 corePoolSize(即线程池中无运行的线程),则创建一个新线程来执行任务。

(2)在线程池完成预热之后(当前线程池中有一个运行的线程),将任务加入 LinkedBlockingQueue。

(3)线程执行完(1)中的任务后,会在一个无限循环中反复从 LinkedBlockingQueue 获取任务来执行。

SingleThreadExecutor 的适用场景: 适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。

 

CachedThreadPool 详解

CachedThreadPool 是一个会根据需要创建新线程的线程池,下面是创建 CachedThreadPool 的源代码。
Java 线程池详解(二):三个现成的线程池详解
CachedThreadPool 的 corePoolSize 被设置为 0,即 corePool 为空;

maximumPoolSize 被设置为 Integer.MAX_VALUE,即 maximumPool 是无界的。这里把 keepAliveTime 设置为 60L,意味着 CachedThreadPool 中的空闲线程等待新任务的最长时间 60 秒,空闲线程超过 60 秒后将会被终止。

FixedThreadPool 和 SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列。CachedThreadPool 使用没有容量的 SynchronousQueue 作为线程池的工作队列,但 CachedThreadPool 的 maximumPool 是无界的。

这意味着,如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新线程。极端情况下,CachedThreadPool 会因为创建过多线程而耗尽 CPU 和内存资源。

CachedThreadPool 的 execute() 方法的执行示意图如下图所示:
Java 线程池详解(二):三个现成的线程池详解
对上图的说明如下:

(1)首先执行 SynchronousQueue.offer(Runnable task)。如果当前 maximumPool 中有空闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute() 方法执行完成;否则执行下面的步骤(2)。

(2)当初始 maximumPool 为空,或者 maximumPool 中当前没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤(1)将失败。此时 CachedThreadPool 会创建一个新线程执行任务,execute() 方法执行完成。

(3)在步骤(2)中新创建的线程将任务执行完后,会执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个 poll 操作会让空闲线程最多在 SynchronousQueue 中等待 60 秒钟。如果 60 秒钟内主线程提交了一个新任务(主线程执行步骤(1)),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲 60 秒的空闲线程会被终止,因此长时间保持空闲的 CachedThreadPool 不会使用任何资源。

前面提到过,SynchronousQueue 是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作,反之亦然。CachedThreadPool 使用 SynchronousQueue,把主线程提交的任务传递给空闲线程执行。CachedThreadPool 中任务传递的示意图如下图所示。

Java 线程池详解(二):三个现成的线程池详解

CachedThreadPool 的适用场景: CachedThreadPool 是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。

参考书籍:《Java 并发编程的艺术》—— 方腾飞著

相关文章: