Java 线程池笔记

下面是一种最为简单的线程创建和回收的方法。它创建了一个线程,并在 run() 方法结束后,自动回收该线程。

1
2
3
4
5
6
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();

这段代码在简单的应用系统中没有太大问题。但在真实环境下,这种为“为每一个任务分配一个线程”的方法存在较多的缺陷,它没有限制可创建线程的数量,并且会频繁地创建和销毁线程。我们可以通过合理地使用线程池来克服这些缺陷。

Executors

Executors 通过静态工厂方法提供了 4 种线程池。

  • newFixedThreadPool(int):一个固定长度的线程池。每当提交一个任务时就创建一个线程,直至达到线程池的最大数量,这时线程池的规模将不再变化。此后有新任务提交时,线程池中若有空闲线程,则立即执行,若没有,则在任务队列等待。
  • newCachedThreadPool():可缓存的无界线程池。当任务超过线程数量时则创建新线程,当线程空闲时间超过 60s 时则自动回收。
  • newSingleThreadExecutors():只有一个线程的线程池,保证任务按照其提交的顺序执行。
  • newScheduledThreadPool(int):创建一个定时或周期性执行任务的线程池,该方法可指定线程池的核心线程个数。

ThreadPoolExucutor

上述的简单工厂方法最终都是调用 ThreadPoolExecutor 的构造函数来实现的。

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}

参数说明:

  • corePoolSize(线程池基本大小):当提交一个任务到线程池时,若线程池已创建的线程数小于 corePoolSize,即使存在空闲线程也会创建一个新线程来执行该任务。
  • maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于 maximumPoolSize,则线程池会创建新的线程去执行任务。
  • keepAliveTime(线程活动保持时间):当线程池的线程个数多于 corePoolSize 时,线程的空闲时间超过 keepAliveTime 则会终止。但调用 allowCoreThreadTimeOut(boolean) 方法也可将此超时策略应用于核心线程。
  • TimeUnit(线程活动保持时间的单位):DAYS、HOURS、MINUTES、SECONDS、MILLISECONDS、MICROSECONDS、NANOSECONDS
  • workQueue(任务队列):用于保存等待执行任务的阻塞队列。主要有以下几种:
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO (先进先出) 排序元素。静态工厂方法 Executors.newFixedThreadPool() 使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • threadFactory:创建线程的工厂。
  • handler(饱和策略):当任务太多时来不及处理时,如何拒绝任务。

流程

流程
当提交一个新任务到线程池时,线程池的处理流程如下:

  1. 首先线程池判断核心线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。corePoolSize 为 0 时是一种特殊情况, 此时即使工作队列没有饱和, 向线程池第一次提交任务时仍然会创建新的线程。
  2. 其次线程池判断是否有空闲线程?若是,则使用空闲线程来执行任务。否则进入下个流程。
  3. 接着线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
  4. 最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。

分析

我们分析下 newFixedThreadPool 和 newCachedThreadPool 的构造函数,来验证下整个流程。

newFixedThreadPool

1
2
3
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());

假设这是一个线程数量为 n 的 newFixedThreadPool,当我们提交 n 个任务到该线程池时,由于此时核心线程池未满,线程池会创建 n 个工作线程来执行任务。当我们再提交新的任务时,由于核心线程池已满且没有空闲线程,就会进入流程 3。由于 LinkedBlockingQueue 是个无界队列,因此之后不管再加多少个任务,都不会走到流程 4.
分析可得 newFixedThreadPool 确实是个固定长度的线程池。

newCachedThreadPool

1
2
3
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());

当我们添加第一个任务到 newCachedThreadPool 时,线程池会创建一个线程去执行任务。之后,当我们添加任务时,由于核心线程池大小为 0,且此时线程池中没有空闲线程,再加上 SynchronousQueue 是一个不储存的阻塞队列,会直接进入流程 4 中,会创建一个新的线程来执行任务。由于 maximumPoolSize 为 Integer.MAX_VALUE,线程池无界而走不进饱和策略。之后当线程空闲时间超过 60s 时则自动回收。

如果我希望 newCacedThreadPool 线程池有个上界,应该怎么做?直接把 maximumPoolSize 改为上界是否可以?

1
new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronusQueue<Runable>());

这时当你提交 3 个任务时,一切正常。之后再提交任务,就会进入饱和策略。此时确实有了上界,但多余的任务会进入饱和策略,而不是储存到队列里。

1
new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runable>());

如果把工作队列改为无界的 LinkedBlockingQueue?情况更糟了,线程池中只有一个线程。因为 LinkedBlockingQueue 是个无界队列,所以会停留在流程 3,而不是去创建新的线程。
正确的做法其实是调用 newFixedThreadPool 的构造函数并设置 allowCoreThreadTimeOut(true)。

参考