拆轮子:全面剖析 ThreadPoolExecutor(1)-基本信息

本文目录:

概述

首先思考,为什么要复用线程,使用线程池?需要的时候新创建一个,不用的时候再关闭不就挺好的?

线程的创建和销毁涉及到系统调用等等,是比较消耗 CPU 资源的。如果线程本身执行的任务时间短,创建和销毁线程所耗费的资源就会占很大比例,对系统起源和运行时间上是一个瓶颈。

我们的任务会被封装在 Runnable 中,在线程启动的时候去执行。那么,如果这些任务执行都很快,又不想把时间浪费在线程的创建和销毁上,怎么办?这时候线程池 ThreadPoolExecutor 就派上了用场,它管理着那些创建好的线程,并根据不同的策略,把任务一个一个地执行下去,不用急着关闭。

线程池本身就是享元模式的应用。同时对线程有合理的退出机制,如果线程长时间没有任务可以执行,也是很消耗系统资源的。因此,我们在配置线程池的时候这方面也要考虑好。因为线程被线程池管理着,所以还可以对线程的执行进行监控。

根据网上一些优秀的讲线程池的博文分析,总结一下使用线程池会有这样的优势:

  • 降低资源消耗,线程频繁的创建和销毁的消耗可以降低。
  • 提高响应速度,两个线程如果被分配到不同的 CPU 内核上运行,任务可以真实的并发完成。
  • 提供线程的可管理性,比如对线程的执行任务过程的监控。

ThreadPoolExecutor 是大神 Doug Lea 在 java.util.concurrent 包提供一个大轮子,很好地满足了各个应用场景对于线程池的需求。大部分开源项目和应用,都是使用该线程池。

大部分人停留在对基本配置的熟悉上面。简单来说,就是熟读该产品的使用说明书。对于大部分使用者和大部分应用场景来说,这个是够的。但我们除了熟悉轮子的使用姿势,知其然更要知其所以然,熟悉原理和机制,这样才能够去处理一些棘手的问题,深挖出一些优化点,最大限度地去提高线程的使用性能。同样也为我们的代码设计有打来的反馈,吸收大神的精华,提高代码逼格。

本次的 ThreadPoolExecutor 分析,基于 JDK 1.8。建议在计算机上阅读该文章。因为篇幅过长,对整体文章进行了切割,分成了几篇。

寻根问祖:继承关系

这里沿着线程池基因的痕迹,去探索线程的父辈们。线程池继承了他们的什么特殊才华,或者实现了他们定下的什么规矩。

整个继承关系如下:

继承关系

Executor 接口,只是一个执行器,只定义了一个 execute 用来执行 Runnable 任务。

public interface Executor {
    void execute(Runnable command);
}

子类的实现,可以对任务的执行制定各种策略,比如:

  • 是同步执行还是异步执行。
  • 是直接执行,还是是放入任务队列延迟执行。

ThreadPoolExecutor 作为它的实现类,完善了整个执行任务的策略,内部管理着工作线程队列和任务队列,实现了多线程并发执行任务。

ExecutorService 接口,为执行器增加了对执行器生命周期的管理。还扩展了可执行任务的类型,对 Callable 类型任务的支持,返回 Future 来提供给调用者。

涉及到的生命周期的处理方法有:

  • shutdown
  • shutdownNow
  • awaitTermination
  • isTerminated
  • isShutdown

可以看到主要是对执行器对一些关闭处理。ThreadPoolExecutor 实现了这些方法,比如使用 shutdown 来关闭线程池。但是线程池不会马上关闭,所以还可以继续调用 awaitTermination 方法来阻塞等待线程池正式关闭。

AbstractExecutorService 抽象类,添加一些执行多任务的工具方法,并对 ExecutorService 的 submit 方法提供默认实现,可供子类直接使用。

有始有终:生命周期

线程池也是有生命的,和世界万物一样,有始有终。

要理解 JDK 给我们造的线程池轮子 ThreadPoolExecutor,有必要熟悉一下它生命的每个阶段。

这些阶段对应的具体的状态有这 5 种:

  • RUNNING,运行状态。接受新的任务,并且把任务入列。线程池里的线程会从队列中取任务执行。
  • SHUTDOWN,关闭状态。无法接受新任务,但会继续执行队列中的任务。这个期间,如果线程池没有线程,但是任务队列中还有任务,那么线程池会开启新的线程把任务执行完。当线程池在 RUNNING 的情况下,调用 shutdown 或者 finalize 方法会进入到 SHUTDOWN 状态。
  • STOP,停止状态。无法接受新任务,不会继续执行队列中的任务,并且会对线程池中正在执行的线程设置中断。当线程池处于 RUNNING 或者 SHUTDOWN 的情况下,调用 shutdownNow 方法会进入 STOP 状态。如果还有任务未执行完成,shutdownNow 会把未完成的任务返回。
  • TIDYING,整理状态。当所有任务结束,工作者数量也为 0,会进入该状态并且调用 terminated() 钩子方法。这个钩子方法由子类实现,用来执行进行最后的关闭操作。
  • TERMINATED,终止状态。terminated() 钩子方法执行完毕。

这个生命周期如下:

线程池生命周期

在我们平时的使用中,基本处于 RUNNING 状态。在进入 SHUTDOWN 或者 STOP 后,还会做一些收尾工作,等都处理好了,线程池进入 TIDYING 状态,调用完钩子方法 terminated() 后就进入 TERMINATED 状态了,线程池走完了它的一生。

妙手生花:使用姿势

这里先聊聊线程池的一些配置和常规使用。线程池的构造如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler);

这些参数都有什么意义,这里做个简单介绍。

配置信息

  • corePoolSize,核心线程数。这个也就是线程常务工作线程,如果没有设置核心线程也要受超时控制(allowCoreThreadTimeout)的话,基本上线程池不关闭都在不会关闭。只要当我们执行某个任务时,没有达到核心线程数的话,是会直接创建新的线程。
  • maximumPoolSize,最大线程数量。当核心线程数满了,然后队列又满了,这个配置开始生效。如果当前有效工作线程数量小于最大线程数量,会再创建新的线程。
  • keepAliveTimeunit,线程池中线程的空闲时间限制。也称为保活时间。TimeUnit 用来指定 keepAliveTime 的时间单位。这个时间在线程最后调用阻塞队列的 workQueue 的 poll 方法时会设置 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) ,这里用纳秒是因为外部传到线程池的时间会做一次转化 this.keepAliveTime = unit.toNanos(keepAliveTime);
  • workQueue,等待执行的任务队列,为阻塞队列 BlockingQueue 的实现。有个点要注意,如果等待队列选择了无界队列,比如没有设置最大容量的 LinkedBlockingQueue,会导致 maximumPoolSize 的设置失效。因为线程池,创建线程会先满足 corePoolSize 的限制,然后再加入任务队列 workQueue。只有任务队列不能再加任务的时候,线程池才会继续创建线程,直到线程数量到达 maximumPoolSize。而这里使用了无界队列,不会满,也就不会去创建数量大于 corePoolSize 的线程数。
  • ThreadFactory,用来创建新线程,新线程的命名、是否是守护线程、优先级等等都可以使用它。

  • handler,RejectedExecutionHandler 实例,当线程池没线程可用,任务队列也为空的情况下,会被任务交给这个 handler 处理。线程池默认使用 AbortPolicy 策略,直接抛出异常。

这里再补充一个配置

  • allowCoreThreadTimeout,表示核心线程是否也要受超时时间 keepAliveTime 的限制。如果为 true,其实就没有核心线程的概念,所有的线程空闲时间达到 keepAliveTime,都可以执行线程退出操作。

常规线程池

Executors 工具类提供了多种比较常规的线程池配置,具体使用哪一种要看实际的业务场景。主要有这几种:

  • FixedThreadPool
  • SingleThreadPool
  • CachedThreadPool
  • ScheduledThreadPool

具体如下的配置和适用场景如下:

固定线程数线程池

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

核心线程数和最大线程数相同,超时时间为 0,使用无界队列。

如果没有设置 allowCoreThreadTimeout 为 true 的话,这些固定线程会一直待在线程池里,如果中途有某个线程发生异常退出了,线程池会马上新起一个线程补上,直到使用 shutdown 关闭线程池。

因为任务队列是无界队列,在正常运行的状态下,不会有任务被 reject 掉的。

适用于每个时间段执行的任务数相对稳定,数量不大,执行时间又比较长的场景

单一线程数线程池

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

核心线程数和最大线程数都为 1,超时时间为 0,使用无界队列。

和固定线程数相似,只是线程数为固定为 1。和固定线程池不一样,这个线程池不允许重新配置,并且增加新的线程。为了满足这个需求,做了一个代理线程池 FinalizableDelegatedExecutorService,继承自 DelegatedExecutorService,把单线程线程池包裹住,代理线程池的行为,并且不再提供 setMaxmiumPoolSize 等方法来重新配置线程池。

这个线程池就没有并发问题了,都在这个线程执行的任务会顺序执行。

适合那些任务执行时间短,实时性要求又不高的场景。

缓存线程数线程池

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

核心线程数为0,最大线程数不做限制,超时时间为 60 秒,使用有界队列 SynchronousQueue,该队列没有实际容量,只用来做生产者和消费者之间的任务传递。

所以该线程池,在每个任务到达,没有线程可以执行会直接再启动一个。线程执行完任务后,没有新任务到达,会有 60s 的保活时间,这段时间没有新任务到达后线程就会直接关闭。

适用于那些任务量大,然后耗时时间比较少的场景。比如 OkHttp 的线程池的配置就和缓存线程数线程池的配置一样。

周期执行任务线程池

public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

直接创建一个 ScheduledThreadPoolExecutor。

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE,
    DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
    new DelayedWorkQueue(), threadFactory, handler);
}

核心线程数有设置,但最大线程数不做限制,使用了一个可延迟获取任务的无界队列 DelayedWorkQueue,可以实现定期执行任务的特点。

所以可以用来执行定时任务,或者有固定周期执行的任务。

猜你喜欢

转载自blog.csdn.net/firefile/article/details/80513802
今日推荐