如果采用同步的方法,用户会等很久才能等到系统的响应,这样的体验显然不好
于是,我们引入异步,即用户只需要提交任务就好,系统会另开一个新线程去处理这个任务
那么,怎么优雅的实现开新线程这个过程呢?我们会遇到以下问题:
- 任务队列的最大容量是多少呢?
- 怎么控制队列长度不超过最大容量呢?
- 程序如何葱任务队列中取出任务来执行呢?
- 任务队列的流程如何实现呢?
- 怎么保证程序最多同时执行多少个任务呢?
- 。。。。。。?
所以,我们要使用线程池
线程池(Thread Pool) 是一种并发编程中常用的技术,用于管理和重用线程。它由线程池管理器、工作队列和线程池线程组成。
线程池的基本概念是,在应用程序启动时创建一定数量的线程,并将它们保存在线程池中。当需要执行任务时,从线程池中获取一个空闲的线程,将任务分配给该线程执行。当任务执行完毕后,线程将返回到线程池,可以被其他任务复用。
为什么需要线程池?
- 线程的管理复杂 (何时新增线程?何时减少空闲线程?)
- 任务存取复杂 (何时接受任务?何时拒绝任务?怎么保证大家不会抢到同一个任务?)
线程池的优势
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
- 实现消峰:当请求突然大量增多时,系统也能平稳运行(这并不是线程池的独占优势,阻塞队列、消息队列等亦能实现消峰)
线程池的创建与参数
大多数程序都有一个基本的默认线程池,但是当程序复杂后多种任务都混合在同一线程池种,且不够灵活。所以我们一般会手动去创建线程池。
怎么更好的理解线程池呢?
我们可以把线程池想象成一个公司,提交到线程池的任务即是工作,线程便是公司中的员工。
那么,我们在这个理解下,来看线程池的参数如何理解
线程池创建时的构造函数参数如下:
1 | public ThreadPoolExecutor(int corePoolSize, |
其中每个参数的意思可以参考源码:
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是之前创建好的线程池,若不传入这个参数,则是使用系统默认的线程池