Java线程池的原理

线程池简介

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

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

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

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

Java的线程池

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

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

1
void execute(Runnable command);

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

1
2
3
<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是一个抽象类,对父接口的一些方法提供了默认实现。

线程池原理

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

1
2
3
4
5
6
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

    直接抛异常。

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

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

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

    丢弃当前任务。

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

    抛弃最久的任务。

    1
    2
    3
    4
    5
    6
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
    e.getQueue().poll();
    e.execute(r);
    }
    }

常见几种线程池

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

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

  1. newCachedThreadPool

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

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

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

线程池的使用

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

1
2
3
4
5
6
7
8
9
10
11
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数量的线程来并行执行
-------------本文结束感谢您的阅读-------------