一、为什么要使用线程

线程池提供了一种限制和管理资源(包括执行一个任务)。每个线程池还维护一些基本统计信息,例如已完成任务的数量。

使用线程池的好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就可立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会较低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

 

二、线程池的工作流程

ThreadPoolExecutor 执行 execute() 方法的示意图(参考自《java并发编程的艺术》):

java 线程池 闭包_java

首先提交任务,当任务提交给线程池后,会先判断 corePoolSize 是否已满,如果没有满,则直接创建线程执行任务;如果满了,则加入队列中。如果队列没有满,则会构成一个生产者和消费者模型。如下图:

java 线程池 闭包_java_02

线程池使用者生产任务,线程池则为消费者,任务存储在 BlockingQueue 中;如果队列满了,返回 false。随后,任务会进入线程池中,判断线程池是否已满,未满了则创建新线程执行任务,满了则拒绝。

 

三、如何创建线程池?

(1) ThreadPoolExecutor 创建线程池



new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds, runnableTaskQueue, handler);



创建线程池需要如下的参数:

  1. corePoolExecutor:核心线程池的基本大小。
  2. runnableTaskQueue:任务队列,用于保存等待执行任务的阻塞队列。有以下几个可供选择:
  • ArrayBlockingQueue:是一个基于数据结构的有界阻塞队列,此队列按照 FIFO 原则对元素进行排序;
  • LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列是按 FIFO 排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool() 使用了这个队列;
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入元素一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法 Executor.newCachedThreadPool 使用了这个队列;
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  1. maxmumPoolSize:线程池最大数量。
  2. ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  3. RejectedExecutionHandle(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,必须采取一种策略处理提交的新任务。这个策略默认情况下是 AbortPolicy,表示无法处理新任务是抛出异常。在 JDK1.5 中 Java 线程池框架提供了一下 4 中策略:
  • AbortPolicy:直接抛出异常;
  • CallerRunPolicy:只用调用者所在线程来运行任务;
  • DiscardOldestPolicy:丢弃队列任务里最近的一个任务,并执行当前任务;
  • DiscardPolicy:不处理,丢弃掉。

也可以根据应用场景来实现 RejectedExecutionHandle 接口自定义策略。如记录日志或持久化存储不能处理的任务。

  • keepAliveTime:线程活动保持时间,线程池的工作线程空闲后,保持存活的时间。所以如果任务很多的话,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。只有当线程池中的线程数大于corePoolSize时,这个参数才会起作用
  • TimeUnit:线程活动保持时间的单位,可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。

 

(2) Executor 创建线程池

a、newFixedThreadPool

FixedThreadPool 被称为可重用固定线程数的线程池。如下源代码:



public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}



java 线程池 闭包_数据结构与算法_03

  1. 如果当前运行的线程数少于 corePoolSize,则创建新线程来执行任务。
  2. 在线程完成预热后(当前运行的线程数等于 corePoolSize),将任务加入 LinkedBlockingQueue。
  3. 线程执行完 1 中的任务后,会在循环中反复从 LinkedBlockingQueue 获取任务来执行。

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

  1. 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize。
  2. 由于 1,使用无界队列 maxmumPoolSize 将是一个无效参数。
  3. 由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数。
  4. 由于使用无界队列,运行中的 FixedThreadPool 不会拒绝任务。

b、newSingleThreadExecutor() 单线程线程池



public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}



SingleThreadExecutor() 是使用单个 worker 线程的 Executor。单线程线程池,那么线程池中运行的线程数肯定是1。 workQueue 选择了无界的 LinkedBlockingQueue,那么不管来多少任务都排队,前面一个任务执行完毕,再执行队列中的线程。从这个角度讲,第二个参数 maximumPoolSize 是没有意义的,因为 maximumPoolSize 描述的是排队的任务多过 workQueue 的容量,线程池中最多只能容纳 maximumPoolSize 个任务,现在 workQueue 是无界的,也就是说排队的任务永远不会多过 workQueue 的容量,那 maximum 其实设置多少都无所谓了。

java 线程池 闭包_数据结构与算法_04

  1. 如果当前运行的线程数少于 corePoolSize(即线程池中无运行的线程),则创建一个新的线程来执行任务。
  2. 线程完成预热后(当前运行的线程数等于 corePoolSize),将任务加入 LinkedBlockingQueue。
  3. 线程执行完 1 中的任务后,会在一个无限循环中反复从 LinkedBlockingQueue 获取任务。

c、newCachedThreadPool

CachedThreadPool 是一个会根据需要创建新线程的线程池。如下源代码:



public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}



corePoolSize 被设置为 0;maximumPoolSize 被设置为 Integer.MAX_VALUE,即 maximumPool 是无界的。将 keepAliveTime 设置为 60L,表明空闲线程等待新任务的时间最长为 60s,超过后则会被终止。

CachedThreadPool 使用没有容量的 SynchronousQueue 作为线程池的工作队列,但是 maximumPool 确是无界的。这意味着,如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新线程。极端情况下,CachedThreadPool 会因为创建过多的线程而耗尽 CPU 和内存资源。

java 线程池 闭包_java_05

  1. 首先执行 SynchronousQueue.offer(Runnable task)。如果当前 maximumPool 中有空闲线程正在执行 SynchronousQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute() 方法执行完成;否则执行步骤 2。
  2. 当初始maximumPool 为空,或者 maximumPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime, TimeUnit.NANOSECODS)。这种情况下步骤 1 将失败。此时 CachedThreadPool 会创建一个新线程执行任务,execute() 方法执行完成。
  3. 在步骤 2 中新创建的线程将任务执行完后,会执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个 poll 操作会让空闲线程最多在SynchronousQueue 中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执行步骤1),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的 CachedThreadPool 不会使用任何资源。

 

第 4 种:newScheduledThreadPool

创建固定长度的线程池,且同时以延迟或者定时的方法来执行任务。

 

四、阻塞队列 BlockingQueue

该类主要提供了两个方法 put() 和 take(),前者将一个对象放到队列中,如果队列以及满了,就等待直到有空闲节点;与后者从 head 去一个对象,如果没有对象,就等待直到有可取的对象。

FixedThreadPool 与 Sing了ThreadPool 都是采用无界的 LinkedBlockingQueue 实现。LinkedBlockingQueue 中引入了两把锁 takeLock 和 putLock,显然分别是用于 take 操作和 put 操作的。即 LinkedBlockingQueue 入队和出队用的是不同的锁,那么 LinkedBlockingQueue 可以同时进行入队和出队操作,但是由于使用链表实现,所有查找速度会慢一些。

CachedThreadPool 使用的是 SynchronousQueue。

线程池对任务队列包括三种:有界队列、无界队列、同步移交。

  • 无界队列:当请求不断增加时队列将无限增加,因此会出现资源耗尽的情况。
  • 有界队列:如 LinkedBlockingQueue,ArrayBlockingQueue 等,可以避免资源耗尽的情况,但是可能出现队列填满后新任务如何处理?执行饱和策略:中止,抛出异常;抛弃:抛弃该任务;调用者运行:将任务返回给调用者。一般任务的大小和线程池的大小一起调节。对于非常大的队列或者无界队列,里面的任务可能会长时间排队等待。可以直接使用同步移交交任务给工作者线程执行。同步移交并不是真正的队列,只是一种在线程之间移交的机制。