ThreadPoolExecutor 线程池底层原理和执行流程

132 阅读4分钟

为什么要用线程池?

池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率,提高线程的复用性。

这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处

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

如何创建线程池

《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors方法 去创建,而是通过 ThreadPoolExecutor自定义线程池 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

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

  • FixedThreadPool(一池固定线程) 和 SingleThreadExecutor(一池一个线程) : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
  • CachedThreadPool(可扩容线程) 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
  • OutOfMemoryError 错误,也就是java内存溢出异常

线程池创建的构造方法

/**
 * 用给定的初始参数创建一个新的ThreadPoolExecutor。
 */
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                          int maximumPoolSize,//线程池的最大线程数
                          long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                          TimeUnit unit,//时间单位
                          BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                          ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                          RejectedExecutionHandler 
handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                           ) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

线程池的饱和策略有哪些

如果当前 同时运行的线程数量 达到最大线程数量 maximumPoolSize 并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy(默认策略) 丢弃任务并抛出RejectedExecutionException异常。 【默认】
  • ThreadPoolExecutor.DiscardPolicy 直接丢弃掉,不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy 丢弃线称队列的旧的任务,将新的任务添加到队列。也就是丢弃队列中最前面的任务。
  • ThreadPoolExecutor.CallerRunsPolicy 使用调用者线程执行此任务。这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。

举个例子: Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)

ThreadPoolExecutor执行流程

  1. 在创建了线程池后,等待提交过来的任务请求。
  2. 当调用execute()方法添加一个请求任务时,线程池会做如下判断:
    • 2.1 如果正在运行的线程数量 小于 核心线程数corePoolSize 时,那么马上创建线程运行这个任务;
    • 2.2 如果正在运行的线程数量 大于或等于 核心线程数corePoolSize,那么将这个任务放入队列;
    • 2.3如果这时候队列满了且正在运行的线程数量 小于 最大线程数maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    • 2.4如果队列满了且正在运行的线程数量 大于或等于 最大线程数maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做超过一定的时间 (超时时间keepAliveTime)时,线程池会判断:
    • 如果当前运行的线程数大于corePoolSize,那么这个线程就被关闭。

参考文章:线称池饱和策略:DiscardOldestPolicy解释