别让用户等太久,用线程池实现异步

2.1k 词

如果采用同步的方法,用户会等很久才能等到系统的响应,这样的体验显然不好
于是,我们引入异步,即用户只需要提交任务就好,系统会另开一个新线程去处理这个任务

那么,怎么优雅的实现开新线程这个过程呢?我们会遇到以下问题:

  • 任务队列的最大容量是多少呢?
  • 怎么控制队列长度不超过最大容量呢?
  • 程序如何葱任务队列中取出任务来执行呢?
  • 任务队列的流程如何实现呢?
  • 怎么保证程序最多同时执行多少个任务呢?
  • 。。。。。。?

所以,我们要使用线程池

线程池(Thread Pool) 是一种并发编程中常用的技术,用于管理和重用线程。它由线程池管理器、工作队列和线程池线程组成。
线程池的基本概念是,在应用程序启动时创建一定数量的线程,并将它们保存在线程池中。当需要执行任务时,从线程池中获取一个空闲的线程,将任务分配给该线程执行。当任务执行完毕后,线程将返回到线程池,可以被其他任务复用。

为什么需要线程池?

  • 线程的管理复杂 (何时新增线程?何时减少空闲线程?)
  • 任务存取复杂 (何时接受任务?何时拒绝任务?怎么保证大家不会抢到同一个任务?)

线程池的优势

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行
  • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 实现消峰:当请求突然大量增多时,系统也能平稳运行(这并不是线程池的独占优势,阻塞队列、消息队列等亦能实现消峰)

线程池的创建与参数

大多数程序都有一个基本的默认线程池,但是当程序复杂后多种任务都混合在同一线程池种,且不够灵活。所以我们一般会手动去创建线程池。
怎么更好的理解线程池呢?
我们可以把线程池想象成一个公司,提交到线程池的任务即是工作,线程便是公司中的员工
那么,我们在这个理解下,来看线程池的参数如何理解
线程池创建时的构造函数参数如下:

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

其中每个参数的意思可以参考源码:

  • corePoolSize:核心线程数,即会长期存在的线程数,也就是不会被开除的正式员工
  • maximumPoolSize:最大线程数,当任务较多时,通过创建新的临时线程所能达到的最大线程数,也就是公司缺人时紧急招来的临时员工
  • keepAliveTime, unit:二者共同组成临时线程的存活时间,当临时线程空闲超过这个时间后,会被销毁,也就是临时员工多久不干活就会被开除
  • workQueue:存放任务的一个阻塞队列,也就是工作备忘录
  • threadFactory:线程工程,可以定制线程属性(名称、优先级等),跟踪和监控线程
  • handler:拒绝策略,默认当队列满时拒绝任务(也可以设置为重新排队等策略)

线程池的工作方式

想要知道各个参数该怎么设置,就得先知道线程池的工作方式,接下来我们将分几种情况来讨论:

  • 刚开始,没有任何线程和任务
    接到新的任务(工作)后,线程池会创建一个新线程(招聘一个正式员工)来完成这个任务

  • 线程数已达到 corePoolSize,即正式员工已经招满了
    新来的任务会被放进 workQueue 中,也就是在备忘录中记下这个任务

  • workQueue 满了,即备忘录已经写满,不能再写了
    线程池会创建临时线程,也就是招聘临时工

  • 线程数达到 maximumPoolSize,即员工总数也满了
    此时临时线程也不会再被创建,多的任务会按照拒绝策略 handler 处理,默认是直接拒绝

  • 临时线程的空闲时间达到 keepAliveTime 后,线程会被消耗
    此时临时线程会被销毁,直到线程数达到 corePoolSize

设置合理的线程池参数

设计线程池参数时,需要考虑任务是 IO 密集型还是计算密集型

  • 计算密集型
    吃CPU,比如音视频处理、图像处理、数学计算等
    一般将 corePoolSize 设置为 CPU 核心数加一,“加一”可以理解为一个备用线程,来处理其他任务。这样做可以充分利用每一个 CPU 核心,减少线程间的频繁切换,降低开销。
    maximumPoolSize 的设定没有严格的规则,一般可以设置为核心线程数的两倍到三倍。

  • IO 密集型
    主要消耗带宽或内存硬盘的读写资源,对 CPU 的利用率不高,如查询数据库或等待网络消息传输
    这种情况下可以适当增大 corePoolSize 的值,因为 CPU 本来就是空闲的

提交任务

因为有了线程池的封装,所以提交任务就很简单了,如下:

1
CompletableFuture runAsync(Runnable runnable, Executor executor)
  • runnable 是要执行的任务
  • executor 是之前创建好的线程池,若不传入这个参数,则是使用系统默认的线程池