Java 类库提供了几种线程池? 分别有什么特点?

文章目录

      • 线程池
      • Executors 工厂类
      • 通过 Executor 框架 API 来创建线程池
        • ThreadPoolExecute 构造函数
      • 使用线程池常见问题
      • 线程池大小的选择

线程池

Java语言虽然内置了多线程支持,启动一个新线程非常方便,但是,创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间

如果可以复用一组线程:
Java 类库提供了几种线程池? 分别有什么特点?_第1张图片
在没有任务时线程处于空闲状态,当请求到来:线程池给这个请求分配一个空闲的线程,任务完成后回到线程池中等待下次任务(而不是销毁)。这样就实现了线程的重用。

为每个请求都开一个新的线程虽然理论上是可以的,但是会有缺点:

  • 线程生命周期的开销非常高。每个线程都有自己的生命周期,创建和销毁线程所花费的时间和资源可能比处理客户端的任务花费的时间和资源更多,并且还会有某些空闲线程也会占用资源。
  • 程序的稳定性和健壮性会下降,每个请求开一个线程。如果受到了恶意攻击或者请求过多(内存不足),程序很容易就奔溃掉了。

所以说:我们的线程最好是交由线程池来管理,这样可以减少对线程生命周期的管理,一定程度上提高性能。

Executors 工厂类

前面已经整理了两个常见的创建方式:继承 Thread,重写Run() 方法 和 实现 Runnable 接口重写 run() 方法。

从 JDK1.5 开始,Java 提供 Callable 接口,提供了另一种方式:实现 Callable 接口创建线程,这个方式可以获取到线程执行的返回值、以及是否执行完成等信息。

在简单应用中前两种创建方式可以满足,但是在业务量大时,随着线程数量的增多,反而会耗尽 CPU 和内存资源,如果处理不当就会 OOM。因此就很有必要引入线程池

在线程频繁调度的场景中,JDK1.5 以前,必须手动打造线程池,来节约系统开销;而从 JDK1.5 开始,Java 提供了一个 Excutors 工厂类来生产线程池,进行线程控制

Executors 目前提供的 5 种不同的线程池创建配置:
Java 类库提供了几种线程池? 分别有什么特点?_第2张图片
newCachedThreadPool() 一种用来处理大量短时间工作任务的线程池

  • 它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程
  • 如果线程闲置的时间超过 60 秒,则被终止并移出缓存
  • 长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列

newFixedThreadPool(int nThreads) 重用指定数目(nThreads)的线程

  • 其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的
  • 如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现
  • 如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads

newSingleThreadExecutor() 线程池中的线程数量为 1,操作一个无界的工作队列

  • 保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态
  • 不允许使用者改动线程池实例,因此可以避免其改变线程数目

newSingleThreadScheduledExecutor() 创建只有一条线程的线程池

  • 创建的是个 ScheduledExecutorService
  • 可以在指定延迟后执行线程任务

newWorkStealingPool(int parallelism) Java 8 才加入这个创建方法

  • 其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序

newScheduledThreadPool(int corePoolSize) 创建具有指定线程数的线程池

  • 可以在指定延迟后执行线程任务
  • 即使线程是空闲的,也被保存在线程池内

通过 Executor 框架 API 来创建线程池

Java 类库提供了几种线程池? 分别有什么特点?_第3张图片

  • Executor 是一个基础的接口,其初衷是将任务提交和任务执行细节解耦,这一点可以体会其定义的唯一方法:
void execute(Runnable command)

Executor 的设计是源于 Java 早期线程 API 使用的教训,开发者在实现应用逻辑时,被太多线程创建、调度等不相关细节所打扰。就像我们进行 HTTP 通信,如果还需要自己操作 TCP 握手,开发效率低下,质量也难以保证

从源码上看,无论是 newFixedThreadPool() 方法、newSingleThreadExecutor() 方法,还是 newCachedThreadPool() 方法,其背后均使用了 ThreadPoolExecutor
Java 类库提供了几种线程池? 分别有什么特点?_第4张图片
Java 类库提供了几种线程池? 分别有什么特点?_第5张图片

  • 工作队列负责存储用户提交的各个任务,这个工作队列,可以是容量为 0 的 SynchronousQueue(使用 newCachedThreadPool),也可以是像固定大小线程池(newFixedThreadPool)那样使用 LinkedBlockingQueue
private final BlockingQueue<Runnable> workQueue
  • 内部的“线程池”,这是指保持工作线程的集合,线程池需要在运行过程中管理线程创建、销毁。例如,对于带缓存的线程池,当任务压力较大时,线程池会创建新的工作线程;当业务压力退去,线程池会在闲置一段时间(默认 60 秒)后结束线程
private final HashSet<Worker> workers = new HashSet<>()
  • 线程池的工作线程被抽象为静态内部类 Worker,基于AQS实现
  1. ThreadFactory 提供上面所需要的创建线程逻辑
  2. 如果任务提交时被拒绝,比如线程池已经处于 SHUTDOWN 状态,需要为其提供处理逻辑,Java 标准库提供了类似ThreadPoolExecutor.AbortPolicy等默认实现,也可以按照实际需求自定义

ThreadPoolExecute 构造函数

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

构造函数的参数释义

  • corePoolSize:核心线程数,长期驻留的线程数目
  • maximumPoolSize:线程不够时能够创建的最大线程数
  • keepAliveTime:当线程池中线程数量超过 corePoolSize 时,空闲线程的存活时间
  • Timeunit:keepAliveTime 的单位
  • workQueue:工作队列,必须是 BlockingQueue
  • threadFactory:线程工厂,用于创建线程,一般用默认的即可
  • handler:拒绝策略,当任务太多来不及处理,如何拒绝任务

有关 workQueue 以及 handler 参数

  • 参数 BlockingQueue workQueue,是用于存放提交尚未被执行的任务的队列,类型是 BlockingQueue 接口的对象,用于存放 Runnable 对象

  • 参数 RejectedExecutionHandler handler 是指当任务数量超过系统承载能力时,该如何处理?其中 JDK 提供了四种拒绝策略

通过配置不同的参数,我们就可以创建出行为大相径庭的线程池,这就是线程池高度灵活性的基础

使用线程池常见问题

线程池虽然为提供了非常强大、方便的功能,使用不当同样会导致问题:

  • 避免任务堆积。 newFixedThreadPool 是创建指定数目的线程,但是其工作队列是无界的,如果工作线程数目太少,导致处理跟不上入队的速度,这就很有可能占用大量系统内存,甚至是出现 OOM。诊断时,你可以使用 jmap 之类的工具,查看是否有大量的任务对象入队
  • 避免过度扩展线程。我们通常在处理大量短时任务时,使用缓存的线程池,比如在最新的 HTTP/2 client API 中,目前的默认实现就是如此。我们在创建线程池的时候,并不能准确预计任务压力有多大、数据特征是什么样子(大部分请求是 1K 、100K 还是 1M 以上?),所以很难明确设定一个线程数目
  • 如果线程数目不断增长(可以使用 jstack 等工具检查),也需要警惕另外一种可能性,就是线程泄漏,这种情况往往是因为任务逻辑有问题,导致工作线程迟迟不能被释放。建议排查下线程栈,很有可能多个线程都是卡在近似的代码处
  • 避免死锁等同步问题
  • 避免在使用线程池时操作 ThreadLocal,工作线程的生命周期通常都会超过任务的生命周期

线程池大小的选择

  • 如果我们的任务主要是进行计算,那么就意味着 CPU 的处理能力是稀缺的资源,我们能够通过大量增加线程数提高计算能力吗?往往是不能的,如果线程太多,反倒可能导致大量的上下文切换开销。所以,这种情况下,通常建议按照 CPU 核的数目 N 或者 N+1
  • 如果是需要较多等待的任务,例如 I/O 操作比较多,可以参考 Brain Goetz 推荐的计算方法
线程数 = CPU 核数 × 目标 CPU 利用率 ×(1 + 平均等待时间 / 平均工作时间)

你可能感兴趣的:(Web,线程池,Executor,Executors)