# 线程池简介

线程池是用于实现计算机程序中执行并发性的软件设计模式。线程池维护多个线程,等待由监督程序分配任务以便并发执行。

相较于普通创建线程的方式,线程池有以下两个个优点:

  1. 线程可以复用。线程使用完不会销毁,避免频繁创建和销毁线程降低系统的效率。
  2. 可以减少并发线程的数量。线程池可以控制线程的数量,防止创建大量线程降低性能。
  3. 提供定时执行、定期执行、单线程、并发数控制等功能。

当线程使用的时间较短,或者需要大量创建线程时,就可以使用线程池。

# Java 的线程池

ThreadPoolExecutor 类实现了线程池,下图是 ThreadPoolExecutor 类的继承关系。

Executor 接口是线程的执行接口,只有一个方法 execute。

void execute(Runnable command);

ExecutorService 接口继承 Executor 接口,添加了关闭线程、提交线程等方法。其中,提交线程的方法就相当于 execute 方法。

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

execute 方法和 submit 方法的区别是:

  1. 接受参数不一样。
  2. submit 有返回值,execute 没有返回值。
  3. submit 方便异常处理。

AbstractExecutorService 是一个抽象类,对父接口的一些方法提供了默认实现。

# 线程池原理

下面是线程池的几个核心属性。

private final BlockingQueue<Runnable> workQueue;	// 线程的阻塞队列
private volatile ThreadFactory threadFactory;		// 线程的工厂
private volatile RejectedExecutionHandler handler;	// 线程的饱和策略
private volatile long keepAliveTime;				// 线程超出核心数量的空闲线程的存活时间
private volatile int corePoolSize;					// 线程的核心数量
private volatile int maximumPoolSize;				// 线程的最大数量
  1. 若当前线程数量少于 corePoolSize,就创建一个新的线程,无论线程池中是否有空闲的线程;
  2. 若当前线程数量大于 corePoolSize 小于 maximumPoolSize,就将线程放入阻塞对列等待执行;
  3. 若当前线程数量小于 maximumPoolSize,阻塞对列已满,就直接创建新的线程进行处理;
  4. 若当前线程数量等于 maximumPoolSize,并且阻塞队列已满,就使用饱和策略。

Java 有以下几种阻塞队列:

  1. ArrayBlockingQueue

    由数组实现的阻塞队列,FIFO,有界。

  2. LinkedBlockingQueue

    由链表实现的阻塞队列,FIFO,无界。

  3. SynchronousQueue

    没有存储空间的阻塞队列,任务提交给它之后必须要交给一条工作线程处理,如果当前没有空闲线程,则新建一个线程。无界。

  4. PriorityBlockingQueue

    具有优先级的阻塞队列。

线程池有四种饱和策略:

  1. AbortingPolicy

    直接抛异常。

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
  2. CallerRunsPolicy

    只用调用者所在的线程执行任务。

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
  3. DiscardOldestPolicy

    丢弃当前任务。

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    }
  4. DiscardPolicy

    抛弃最久的任务。

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll();
            e.execute(r);
        }
    }

# 常见几种线程池

Executors 类提供了几个构建线程池的方法。

查看 Executors 类的源码可知,Executors 就是通过构造 ThreadPoolExecutor 实现的线程池。

  1. newCachedThreadPool

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    • 线程池核心线程数量为 0,最大线程数量为 Integer 最大值。
    • 只要线程空闲 60s,就会被销毁。
    • 使用 SynchronousQueue 阻塞队列,只要没有空闲线程,就会创建新的线程来执行任务。
  2. newFixedThreadPool

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    • 线程池大小固定,核心数量和最大数量均为 N=nThreads。
    • 超多核心线程数量的线程一旦空闲下来就会被销毁,但是这里该参数无效,因为最大线程数量等于核心线程数量。
    • 阻塞队列使用基于链表的阻塞队列,所以阻塞队列没有大小限制,可以无限接受任务。
  3. newSingleThreadExecutor

    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
    • 核心线程和最大线程数量都为 1。
    • 使用基于链表的阻塞队列。

# 线程池的使用

使用起来十分简单。例如,使用固定大小的线程池。

public static void main(String[] args) {
    ExecutorService cachedThreadPool = Executors.newWorkStealingPool(2);
    for (int i = 0; i < 10; i++) {
        cachedThreadPool.execute(() -> {
            for (int j = 0; j < 10; j++) {
                System.out.println(Thread.currentThread().getName());
            }
        });
    }
    cachedThreadPool.shutdown();
}

只需要创建的一个线程池,然后线程池执行 Runnable 方法就可以了。

然后在实际使用中,我们最好不要直接使用 Executors 创建的线程池,最好我们自己来创建一个 ThreadPoolExecutor。

在阿里巴巴代码规约中有这样一条规则:

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

1)FixedThreadPool 和 SingleThreadPool:

​ 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2)CachedThreadPool 和 ScheduleThreadPool:

​ 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

# Spring 中的线程池

Spring 封装了 Java 的多线程的实现,我们一般使用 ThreadPoolTaskExecutor 创建线程池。

配置如下:

使用时直接调用 execute 方法即可。

# 几种线程池总结

方法描述使用场景
newCachedThreadPool必要时创建新线程,空闲线程会被保留 60 秒比较适合处理执行时间比较短的任务
newFixedThreadPool该池包含固定数量的线程,空闲线程会一直被保留可以用于已知并发压力的情况下,对线程数做限制
newSingleThreadExecutor只有一个线程的 “池”,该线程顺序执行每一个提交的任务可以用于保证顺序执行的场景,并且只有一个线程在执行
newScheduledThreadPool用于预定执行而构建的固定线程池适用于需要多个后台线程执行周期任务的场景
newSingleThreadScheduledExecutor用于预定执行而构建的单线程 “池”适用于需要一个后台线程执行周期任务的场景
newWorkStealingPool线程池拥有多个任务队列,可以减少连接数创建当前可用 CPU 数量的线程来并行执行