Java-并发-线程池
0.背景
在 Java 早期,每次创建线程时,都要涉及到线程的创建、销毁以及资源管理,这对于系统的性能和资源利用率是一种浪费。
因此,Java 提供了线程池的概念,以提高线程的管理效率和性能。
-
资源管理优化:传统的线程创建和销毁需要涉及系统资源的分配和释放,而频繁的线程创建和销毁会导致资源的浪费和性能下降。
线程池通过维护一组可重用的线程来减少线程的创建和销毁次数,从而优化了系统资源的利用。
-
线程复用:线程池可以维护一定数量的线程池,这些线程可以被重复利用来执行多个任务,而不必每次都创建新线程。
这样可以避免频繁地创建和销毁线程,提高了系统的性能和效率。
-
任务调度和管理:线程池可以对任务进行调度和管理,可以根据任务的优先级和类型来调度执行线程,提供了更加灵活和可控的任务执行方式。
-
并发控制:线程池提供了一种方便的方式来控制并发执行的线程数量,可以限制同时执行的线程数量,从而避免因线程过多导致系统资源耗尽或性能下降的问题。
1.概念
1.1 关键词
记忆方法:核大存单队略厂(这里和大存单的队伍略长)
1.1.1 核心线程数 corePoolSize
- 含义:指定线程池中保持的活动线程数。除非设置了
allowCoreThreadTimeOut
为true
,否则这些线程不会被回收。 - 作用:核心线程数决定了线程池中保持的最小活动线程数,即使线程处于空闲状态也不会被回收。
1.1.2 最大线程数 maximumPoolSize
- 含义:指定线程池中允许创建的最大线程数。当任务队列已满且核心线程数已达到最大时,线程池会创建新的线程来执行任务,直到达到最大线程数为止。
- 作用:最大线程数决定了线程池中允许创建的最大线程数,用于处理任务队列中的任务。
1.1.3 空闲线程存活时间 keepAliveTime
- 含义:指定空闲线程的存活时间,即当线程池中的线程处于空闲状态且空闲时间超过该值时,这些空闲线程可以被回收。
- 作用:通过设置线程的存活时间,可以控制线程池中线程的数量,避免因线程过多导致系统资源的浪费。
1.1.4 空闲线程存活时间单位 unit
- 含义:指定空闲线程存活时间的单位,可以是纳秒、微秒、毫秒、秒、分钟、小时或天等。
- 作用:单位参数用于指定空闲线程存活时间的时间单位,与
keepAliveTime
参数配合使用。
1.1.5 工作队列 workQueue
- 含义:任务队列,用于保存等待执行的任务。线程池中的线程会从任务队列中取出任务执行。
- 作用:工作队列用于存储提交给线程池但尚未执行的任务,当线程池中的线程处于忙碌状态时,新的任务会被放入任务队列等待执行。
1.1.6 拒绝策略 rejectedExecutionHandler
- 含义:拒绝策略,用于处理当任务无法被线程池执行时的情况。当任务队列已满且无法接受新的任务时,或者线程池已关闭但仍然有任务提交时,会触发拒绝策略。
- 作用:拒绝策略定义了线程池无法处理任务时的行为,包括抛出异常、丢弃任务、阻塞调用者或者执行任务的线程等待一段时间后再尝试执行任务等。选择合适的拒绝策略能够有效地处理任务提交过载的情况,保护线程池和系统的稳定性。
1.1.7 线程工厂 threadFactory
- 含义:线程工厂,用于创建新的线程。当线程池需要创建新线程时,会调用线程工厂的
newThread(Runnable r)
方法来创建线程。 - 作用:线程工厂用于创建线程池中的线程实例,通过自定义线程工厂,可以控制线程的创建过程,如设置线程的名称、优先级、是否为守护线程等。
1.2 线程池的状态
线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。
运行状态 | 是否接收任务 | 是否处理任务 | 状态描述 |
---|---|---|---|
RUNNING | √ | √ | 能接受新提交的任务,并且也能处理阻塞队列中的任务。 |
SHUTDOWN | × | √ | 不再接受新提交的任务,可以继续处理阻塞队列中已保存的任务。 |
STOP | × | × | 不再接受新任务,也不继续处理队列中的任务,会中断正在处理任务的线程。 |
TIDYING | - | - | 所有的任务都已终止了,workerCount(有效线程数)为0。 |
TERMINATED | - | - | 终止状态,在 terminated() 方法(预留)执行完后进入该状态。 |
状态转换如下:
1.3 运行过程
有核心线程,优先用核心线程。核心线程不够用,先往工作队列里放。工作队列都放不下了,再创建非核心线程来执行。实在是不行了,只能进行拒绝。
-
开始,接收到提交的一个任务。
-
判断当前工作线程数与核心线程数的关系
-
工作线程数 < 核心线程数,新建工作线程(核心)并执行。
-
工作线程数 >= 核心线程数
检查工作队列情况
-
工作队列未满,添加任务到工作队列中,等待获取执行。
-
工作队列已满
检查最大线程数
- 未达到最大线程数,新建工作线程(非核心)执行。
- 已经达到最大线程数,进行任务拒绝。
-
-
1.4 理解
注意,以下是我的理解,我半桶水,你记得带自己的思考来阅读喔。
1.4.1 核心线程数和最大线程数
忽略核心线程是否可以销毁
核心线程必定在运行,是我们使用的最小成本。而最大线程是一个兜底机制,不可能无休止的来创建线程,必定要有个度。
线程池中的任务,是依赖核心线程和非核心线程来执行的,不是说跟线程创建销毁跟工作队列没关系,而是侧重点不同,工作队列的目的是缓冲。
1.4.2 工作队列存在的意义
工作队列的存在,是为了提供一个缓冲,帮助线程池保持一个最小可运转的线程数量,避免频繁的创建和销毁线程。
好,我们回顾上面的图,核心线程没满的时候,我们可以新建线程来执行,在核心线程内的数量,我们是很好接受的。
那么,当我的核心线程都不够用了,这个时候,暴力的方式当然是直接新建线程来执行。
- 优点,核心线程在忙碌,我直接搂一个新线程,立马就能执行,效率很高。
- 缺点,涉及到的频繁的创建线程(高成本)。
有了工作队列后,我们核心线程都在忙碌时,选择先将任务放到队列里等一等,实在再放不下了,再来创建线程执行。
我们还可以极端点,假设我们新建一个线程要2s,执行一个任务只要0.5s。
核心线程已满,这个时候,我们是不是等个0.5s就能空闲出一个核心线程,然后就有得执行了。而不是选择立马新建一个,耗费2s。
1.4.3 ThreadPoolExecutor分析
1.4.3.1 关于参数校验
经常会有面试问,核心线程数能设置为0吗?
注意这是构造方法,是做了校验的。
A.参数不合法异常 IllegalArgumentException
- 核心线程数 < 0
- 最大线程数 <= 0
- 最大线程数 < 核心线程数
- 线程存活时间小于0
B.空指针异常NullPointerException
- 工作队列为null
- 线程工厂为null
- 拒绝策略为null
这个构造函数,最少需要5个核心参数喔。
1.4.3.2 核心线程数可以为0吗?
可以
线程池也可以接收任务,线程池会依赖非核心线程来执行任务。
任务首先会被放入工作队列进行缓冲,如果工作队列已满,再根据最大线程数的限制来新建非核心线程来执行任务。
只有在工作队列已满,且线程池中的线程数未达到最大线程数时,才会创建新的线程,如果线程池已达到最大线程数,则不会再创建新线程,根据拒绝策略来处理任务。
然后呢,这个时候也有个问题,由于我们没有核心线程,直接被放到工作队列里,这个队列没满的话,就不会创建线程,相当于在干等。
嗯,就跟我们写的代码一样,让人感到无语。
1.4.3.3 最大线程数可以为0吗?
不可以
首先通过前面可以知道,最大线程数必须比核心线程数大,然后呢,核心线程数不能小于0(参数异常),但是核心线程数可以等于0。
然后呢,还有个校验,是最大线程数是必须大于核心线程数的,所以说呀,这个最大线程数,最小值是1。
哦,sorry,最大线程数的判断直接是maximumPoolSize <= 0
抛异常。
我没骗你,你自己看图。
1.4.4 线程池中的ctl属性
在线程池的代码中,经常能看到一个ctl.get()
在使用,这个ctl
是什么呢?它是其中的一个属性值。
嗯,又是一个高深的设计,看不懂。
暂时理解为这玩意能获取到当前线程池状态和当前工作线程数量
高位表示线程池的运行状态runState
,低位表示线程池的工作线程数量workerCount
,然后框框下面不是还两个of结尾的方法嘛,就方便的拿到这两个内容了。
2.类说明
2.1 Executor接口
- 定义了一个单一的方法
execute(Runnable command)
,用于执行提交的任务。 - 只关注任务的提交方式,不关心任务的执行细节。
2.2 ExecutorService接口
- 是
Executor
接口的子接口,提供了更多的方法用于任务管理和线程池控制。 - 定义了一系列方法来提交任务、关闭线程池、管理任务的执行状态等。
2.3 ThreadPoolExecutor类
- 是
ExecutorService
接口的一个具体实现类,也是Executors
类创建线程池的底层实现。 - 提供了一个灵活的线程池实现,可以通过构造方法来设置各种参数,如核心线程数、最大线程数、任务队列、拒绝策略等。
2.4 Executors类
- 工具类,提供了一系列静态方法用于创建不同类型的内置线程池。
- 通过
Executors
类可以快速方便地创建各种预定义类型的线程池,如固定大小线程池、可缓存线程池等
2.4.1 FixedThreadPool
固定线程数量的线程池,该线程池中的线程数量始终不变。
当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。
若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
2.4.2 SingleThreadExecutor
只有一个线程的线程池。
若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
2.4.3 CachedThreadPool
可根据实际情况调整线程数量的线程池。
线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。
若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
2.4.4 ScheduledThreadPool
给定的延迟后运行任务或者定期执行任务的线程池
3.阻塞队列
常见线程池及其使用到的阻塞队列如下
线程池 | 中文名 | 核心线程数 | 阻塞队列类型 | 工作队列容量 | 特点 | 可能的问题 |
---|---|---|---|---|---|---|
FixedThreadPool | 固定大小线程池 | 固定 | LinkedBlockingQueue | Integer.MAX_VALUE | 只能创建核心线程数的线程,任务队列永远不会被放满 | 工作队列无界,可能导致OOM。 |
SingleThreadExecutor | 单线程线程池 | 1 | LinkedBlockingQueue | Integer.MAX_VALUE | 只能创建一个线程,任务队列永远不会被放满 | 工作队列无界,可能导致OOM。 |
CachedThreadPool | 缓存线程池 | 动态调整 最大Integer.MAX_VALUE |
SynchronousQueue | 无容量 | 最大线程数为Integer.MAX_VALUE,可能会创建大量线程 | 线程数过多,可能导致OOM。 |
ScheduledThreadPool | 定时线程池 | 固定 | DelayedWorkQueue | Integer.MAX_VALUE | 队列添加元素满了之后会自动扩容,最多只能创建核心线程数的线程 | 工作队列每次自动扩容1/2,可能导致OOM。 |
SingleThreadScheduledExecutor | 单线程定时线程池 | 1 | DelayedWorkQueue | Integer.MAX_VALUE | 队列添加元素满了之后会自动扩容,最多只能创建一个线程 | 工作队列每次自动扩容1/2,可能导致OOM。 |
4.拒绝策略
当线程池已经达到了最大线程数限制,且工作队列也已满(无法接受新任务),此时如果继续有新的任务提交给线程池,就会触发拒绝策略。
4.1 默认的拒绝策略
ThreadPoolExecutor中默认的拒绝策略如下
序号 | 名称 | 解释 | 适用场景 |
---|---|---|---|
1 | AbortPolicy(默认) | 拒绝并抛出异常 | 希望立即知道任务被拒绝,并且需要通知调用者处理拒绝的情况时使用。 |
2 | CallerRunsPolicy | 使用调用者线程执行 | 希望调用者线程直接执行任务,一般在不允许失败的、对性能要求不高、并发量较小的场景下使用。 可能会导致主线程阻塞或延迟响应其他请求。 |
3 | DiscardPolicy | 直接丢弃任务 | 对任务执行顺序无要求,可以丢弃一些任务时使用。 |
4 | DiscardOldestPolicy | 丢弃最旧的任务 | 希望尽量保留新提交的任务,并且对于旧任务执行顺序无要求时使用。 |
4.2 自定义拒绝策略
自定义拒绝策略需实现 RejectedExecutionHandler
接口并覆盖 rejectedExecution
方法。
该方法接收两个参数:被拒绝的任务
和执行该任务的线程池
。
在创建线程池时,将自定义的拒绝策略传递给 ThreadPoolExecutor
的构造函数即可:
5.线程池关闭
当线程池不再需要使用时,应该显式地关闭线程池,释放线程资源。
5.1 shutdown()平滑关闭
线程池的状态变为
SHUTDOWN
。线程池不再接受新任务了,但是队列里的任务得执行完毕。
5.2 shutdownNow()暴力关闭
线程池的状态变为
STOP
。线程池会终止当前正在运行的任务,停止处理排队的任务,并返回正在等待执行的任务List。
5.3 awaitTerminatio()阻塞关闭
关闭线程池嘛,肯定也得有个线程来操作这个事情,比如我们的主线程。
上面两个是平滑关闭和暴力关闭,然后啊,这两个操作相对于主线程是异步的在通知线程池,你该关闭了,意思就是不会阻塞主线程。
你看,一个返回void一个返回List待执行任务。
假设我们就想站在这里等这个线程池关闭,需要用到awaitTermination()
方法。效果就是主线程阻塞等待直到线程池的所有任务都执行完毕,并且所有的工作线程都被终止。这样可以确保在主线程继续执行之前,线程池已经完全关闭了。
调用 awaitTermination
方法时,你可以设置一个超时时间,以避免主线程长时间阻塞。如果超时时间到达而线程池依然没有关闭完成,awaitTermination
方法会返回 false。另外,由于该方法可能会抛出 InterruptedException
异常,你需要在调用时进行异常处理,以确保程序的健壮性。
6.获取任务执行结果
6.1 Future
通过提交 Callable 任务给线程池,线程池返回一个 Future 对象,可以用于异步获取任务的执行结果。可以使用 Future 的 get() 方法来阻塞等待任务完成,并获取任务的返回值。
6.2 CompletionService
CompletionService是 ExecutorService 的一个扩展接口,用于管理已完成的任务。
通过将任务提交给 CompletionService,可以异步地获取任务的执行结果,并且可以按照完成顺序获取结果。
6.3 自定义数据结构
可以设计自定义的数据结构来保存任务的执行结果。
例如,使用 ConcurrentHashMap 来保存任务的返回值,任务执行完成后将结果存储在 Map 中,其他线程可以通过键来获取对应的结果。
7.常见问题
7.1 线程池为啥要用阻塞队列BlockingQueue?
-
如果线程池中不使用阻塞队列,而是使用普通的非阻塞队列:
- 队列为空时,尝试取出元素的操作会立即返回空值或者抛出异常,而不会阻塞线程。
- 队列已满时,尝试放入元素的操作也会立即返回失败,可能会抛出异常或者丢弃任务,而不会等待队列有空余位置。
线程池需要自己处理任务的等待和唤醒,增加了线程同步和管理的复杂度。
-
使用了阻塞队列后
- 队列为空时,尝试从队列中取出任务的操作会被阻塞,直到队列中有新的任务被添加进来。
- 队列已满时,尝试往队列中放入新任务的操作也会被阻塞,直到队列中有空余位置可用。
能有效地平衡生产者和消费者之间的速度差异,提高系统的整体效率,并且简化了线程同步和管理的逻辑。
7.2 线程之间如何传递参数?
7.2.1 共享变量
可以使用共享变量来在线程之间传递参数,一个线程设置变量的值,另一个线程读取该变量的值。需要注意线程安全的问题。
7.2.2 共享队列
嗯,和上面的差不多,也是共享数据,通过在线程之间共享内存来实现参数的传递。区别在于数据结构的选择和操作方式。
7.3 线程池的提交时的submit和execute
关键点在于,是否需要任务的结果。
-
execute:适用于提交不需要返回结果的任务,比如日志记录或发送通知。
-
submit:适用于提交需要返回结果或需要知道任务是否完成的任务。
特性 | execute | submit |
---|---|---|
所属类 | interface Executor | interface ExecutorService extends Executor |
返回类型 | void |
Future<T> |
接受的任务类型 | Runnable |
Runnable 或 Callable<T> |
异常处理 | 通过 Thread.UncaughtExceptionHandler 处理 |
异常被封装在返回的 Future 中 |
是否需要返回结果 | 否 | 是 |
用途 | 适用于提交不需要返回结果的任务 | 适用于提交需要返回结果或需要知道任务是否完成的任务 |
8.面试题
今天是2024.5.10,我在xx网上收集了200页的后端面经文章,然后把涉及到
线程池
字眼的问题拿出来了。
找gpt整理后,结果如下。扫了眼gpt还是漏了很多,需要源文本的话,可以私信我。
仅供参考
- 线程池的执行流程是怎样的?
- 线程池的拒绝策略能否自定义?线程池有哪些拒绝策略?
- 怎么设计线程池?
- 线程池的七个参数是什么?线程池的参数和执行流程?
- 为什么要使用线程池?使用线程池有什么好处?
- 线程池怎么实现?线程池的底层原理是什么?
- 线程池为什么要先放满阻塞队列再申请空闲线程,而不是直接创建到最大线程数?
- 使用线程池操作hashmap是线程安全的吗?
- 线程池中阻塞队列有哪几种类型?
- 手撕线程池是怎样的过程?
- 线程池大小数量是不是越大越好?
- 怎么设置线程池的数量?
- 为什么不使用Executors创建线程池?
- 线程池选择线程的方法是什么?
- 线程池与多核心有什么关系?
- 线程池的阻塞队列有哪些实现方式?
- 创建线程池的时候一般怎么初始化?
- 线程池的核心参数 最大线程数和核心线程数区别 什么时候核心线程数的数量达到最大线程数?
- 线程池有哪几种创建方式?
- 线程池的核心参数?
- 线程池有了解吗,了解ThreadPoolExecutor?
- 线程池池化技术,与多线程技术比有什么好处?
- 线程池里边有队列,队列满了之后,如何处理新进入的任务?
- 线程池的状态是怎样的?
- 介绍线程池的核心参数?
- 线程池怎么限流的?
- 在使用线程池的时候使用ThreadLocal会有什么的问题?
- 线程池创建线程的方法?
- 线程池的运行机制是什么?
- 线程池拒绝策略 什么时候拒绝?
- 线程池是怎么实现的?具体流程?
- 线程池如何使用?参数的含义是什么?
- 线程池的好处是什么?你项目中用了哪个线程池?
- 线程池核心数怎么确定的?
- 项目中对线程池的使用还能优化吗?
- 说一下线程池,有哪些参数?核心线程数由什么决定如何设置?
- 线程池怎样停止?
- 怎么实现Linux线程池?
- 你在项目中是怎么使用线程池的?
- 线程池有哪些分类,线程池创建参数有哪些?
- 线程池的任务队列如何实现,空闲线程用什么策略拉取队列的任务?
- 如何创建线程?多线程了解吗?线程池参数?怎么设置一般?
__EOF__

本文链接:https://www.cnblogs.com/yang37/p/18182528.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?