juc基础——线程池-ag九游会j9官方网站
ag九游会j9官方网站-j9九游会登录入口首页新版
ag九游会j9官方网站-j9九游会登录入口首页新版
api
j9九游会登录入口首页新版的解决方案
学堂
社区
控制台
注册
登录
/
/
文章详情
/
juc基础——线程池
2023-09-13
92 浏览
返回文档
江河入海,知识涌动,这是我参与江海计划的第10篇
juc基础——线程池
前言
本文提要:介绍线程池,了解线程池的参数及使用,深入理解线程池工作原理
学习java,juc是绕不过去的一部分内容,juc也就是
java.util.concurrent
包,为开发者提供了大量高效的线程并发工具,方便我们可以开发出更高性能的代码。而juc其实涉及到的内容有很多,主要包含以下部分:
●
线程池
●
并发集合
●
同步器
●
原子变量
●
锁
●
并发工具类
今天我们就来讲其中比较核心和常用的一个内容——
线程池
一、线程池是什么
线程池是一种线程使用模式,我们都知道线程的创建与销毁会消耗资源,为了减少无谓的消耗而引入了池化技术。而线程池、连接池、对象池等池化技术都有一个共同的特征:重复利用。换句话说,存放在线程池里的线程在执行完一个任务后,可能还会存活,等待着下一次执行任务,这样就
避免
每一个任务都需要进行一次线程的创建和销毁。
二、管理线程池
1. 线程池种类
我们可以看到预置的线程池有三种:
●
threadpoolexecutor
最常见的线程池
●
scheduledthreadpoolexecutor
定时线程池,主要用于执行周期性任务
●
forkjoinpool
拆分合并线程池,把一个大任务切分为若干个子任务并行地执行,最后合并得到这个大任务的结果
更细分的每种线程池也有预置的构建方法,这些构建方法在executors类下,一般情况下,我们创建线程池会使用这些预置的构建方法来获取
2. 线程池参数
线程池不单单有存放线程的作用,还具备“管理”的作用,比如我们的任务提交给线程池,线程池会帮我们找到合适的线程来执行;又比如没有任务或任务太多时,线程池也会自动“休息”或“满负荷运行”。这些管理功能需要我们进行参数的配置,不同配置出来的线程池自然效果不同
可以看到,我们最常用的线程池类就是
threadpoolexecutor
,而它的构造方法支持7个参数,这也就是所谓的
线程池的七大参数:
我们先知道这七大参数的意思即可,更具体的影响我们会在后面原理阶段细说。
3. 创建线程池
我们前面说了,
executors
类下为我们预置了不同种类线程池的预建方法,我们简单介绍下
executors
类下常用的五种线程池
●
newsinglethreadexecutor (单线程化)
●
newfixedthreadpool (定长)
●
newscheduledthreadpool (可定期)
●
newcachedthreadpool (可缓存)
●
newworkstealingpool(分治-任务窃取)
但是使用预置的方式创建线程池并非没有弊端,比如
newfixedthreadpool
和
newsinglethreadexecutor
,主要问题是使用的无界队列,堆积的请求处理队列可能会耗费非常大的内存,甚至oom。
newcachedthreadpool
和
newscheduledthreadpool
设置的线程数最大数是integer.max_value,可能会创建数量非常多的线程,甚至oom。
基于这种考虑,所以很多公司的
代码规范会强制要求程序员手动创建线程池
,
如下例子
注意,在上面的创建里,
我们使用了
线程安全的有界队列
,并设定了长度200;
我们自定义了
线程工厂
,这样该线程池创建的线程都有特殊的线程名;
我们选择了
callerrunspolicy 拒绝策略
,一旦线程池满负荷,再往里提交任务就由提交任务的线程自己去运行
其实
拒绝策略
一共有好几种:
●
abortpolicy,这种拒绝策略在拒绝任务时,会直接抛出异常 rejectedexecutionexception (属于runtimeexception),让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
●
discardpolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
●
discardoldestpolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
●
callerrunspolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务,这样做任务可以保证不丢失。
而关于
核心线程、最大线程
的设置是一个
经验问题
,因此一个常见的经验配置如下,但实际上因为可能因为一个项目包含不止一个线程池,所以数目设置仅供参考
任务特性
特点
常用线程池
cpu密集型任务
应配置尽可能小的线程
cpu数 1
io密集型任务
并不是一直在执行任务,则应配置尽可能多的线程
2*cpu数
混合型的任务
可拆分成cpu密集型任务和io密集型任务
另外,很多框架也会提供创建线程池的方式,比如
spring的threadpooltaskexecutor
,但因为我们这里主要还是说juc,所以不再展开。
三、线程池状态
每个线程池都会带有一个原子整型,用来表示自己的状态
按bit位来分,(高3位)标记线程池状态,(低29位)表示线程个数
private final atomicinteger ctl = new atomicinteger(ctlof(running, 0));
那么线程池总共有多少种状态呢?一共有五种,它们的变化关系如下:
●
running
状态说明:线程池处在running状态时,能够接收新任务,以及对已添加的任务进行处理。
状态切换:线程池的初始化状态是running。换句话说,线程池被一旦被创建,
就处于running状态,并且线程池中的任务数为0!
●
shutdown
状态说明:线程池处在shutdown状态时,不接收新任务,但能处理已添加的任务。
状态切换:调用线程池的shutdown()接口时,线程池由running -> shutdown。
●
stop
状态说明:线程池处在stop状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
状态切换:调用线程池的shutdownnow()接口时,线程池由(running or shutdown ) -> stop。
●
tidying
状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为tidying状态。当线程池变为tidying状态时,会执行钩子函数terminated()。terminated()在threadpoolexecutor类中是空的,若用户想在线程池变为tidying时,进行相应的处理;可以通过重载terminated()函数来实现。
状态切换:当线程池在shutdown状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 shutdown -> tidying。
当线程池在stop状态下,线程池中执行的任务为空时,就会由stop -> tidying
●
terminated
状态说明:线程池彻底终止,就变成terminated状态。
状态切换:线程池处在tidying状态时,执行完terminated()之后,就会由 tidying -> terminated
四、线程池的任务提交
在经过上一章的学习后,你应该已经得到了一个executorservice实例了,此时我们可以通过两种方式提交任务(runnable):
tp.execute(runnable)
tp.submit(runnable) 或 tp.submit(task)
这两者我们该怎么选呢,下面我们就来详细讲一下
1. execute
" width="16" class="ne-image-loading-icon">
execute只能用来执行runnable的实现类,而且没有返回值,事实上,这是因为run()本身就没有返回值导致的,因此这种方式,最好用来执行不需要知道结果的任务。
注意:当出现异常时,异常会被catch住,然后throw出来,本线程销毁
2. submit
" width="16" class="ne-image-loading-icon">
●
submit其实内层调用的还是execute,此时传入execute的参数类型是runnablefuture,同时继承runnable 和 future
●
submit可以用来提交runnable或者callable,callable任务带有返回值,因此submit会有一个future返回很合理,通过这个future.get()就能获取返回值;
●
但是runnable任务没有返回值,为什么也有一个future返回呢?其实是你传的runnable 最后还是会被封装成callable后再执行,由于runnable没有返回结果,所以在将runnable包装为callable的时候,会传入一个预期结果null,此时使用get方法返回一个null
注意:当使用的submit时,得益于futuretask中有try-catch来存储异常,所以出现异常,futuretask自己就消化并存起来,并可通过future.get()获取到异常,而不是直接往外抛,因此直接使用submit是不会报错的,线程池里的线程得以存活
五、线程执行异常
我们提交的任务,不总是能顺利执行,一旦出现异常,我们该怎么处理呢?
" width="16" class="ne-image-loading-icon">
1
在我们提供的runnable的run方法中捕获任务代码可能抛出的所有异常,包括未检测异常。这种方法比较简单,也有他的局限性,不够灵活且增大代码量
2
使用submit提交任务,在调用future.get()方法时,会将保存的异常重新抛出
3
在执行任务的过程中,如果出现异常,也可以通过自己写个类,继承threadpoolexecutor并重写该afterexecute()方法来处理,注意,此时线程还是因异常而终止了。
4
当一个线程因为未捕获的异常而退出时,jvm会把这个事件报告给应用提供的
uncaughtexceptionhandler
异常处理器,于是就有了第三种解决任务代码抛出异常的方案:为工作者线程设置uncaughtexceptionhandler,在uncaughtexception方法中处理异常。
那如何为工作者线程设置uncaughtexceptionhandler呢?threadpoolexecutor的构造函数提供一个threadfactory,可以在其中设置我们自定义的uncaughtexceptionhandler,这里不再赘述。
注意:这个方案不适用于使用submit方式提交任务的情况,原因上面也提到了,futuretask的run方法捕获异常后保存,不再重新抛出,意味着runworker方法并不会捕获到抛出的异常,线程也就不会退出,也不会执行我们设置的uncaughtexceptionhandler。
六、线程池执行步骤(简易)
这一章,我们会说一说当一个任务被提交进线程池,会经历什么步骤?我们可以看以下的
精简步骤
:
任务会优先以核心线程运行,当核心线程达到上限时,再往里面提交线程,会把线程放入队列中等待。除非队列放不下了,才会启用非核心线程来运行任务。所以不要用无界队列。如果非核心线程也满了,则执行拒绝策略
" width="16" class="ne-image-loading-icon">
当然上面说的流程是一个大体方向,具体的细节我们只能通过源码来讲,如果你有源码恐惧症,我也给你提了一个精简版源码流程,如果你也喜欢看源码,可以看下一章的源码级流程。
1
我们先去创建一个worker(内含一个线程) 并且把我们的任务传到worker的firsttask变量里
2
worker创建完成以后调用runworker方法;
3
runworker方法里面先把worker自己的firsttask走完(调用runnable.run()),然后会通过gettask()方法从线程池的阻塞队列里面拿缓存的runnable
4
如果当前线程数超核心线程上限,gettask会以 poll(timeout, unit)取任务,一段时间取不到,就会返回null,worker内部thread的run方法因为没有后续任务而走完,线程生命周期结束;
5
如果当前线程数没有超核心线程上限,从队列拿任务时,是以take方法去拿,此时会让线程挂起,直到取到任务再返回
6
取到任务以后再去执行这个任务
七、线程池执行步骤(源码)
1.提交任务,判断是否新建线程执行,或者加入阻塞队列
2.新增worker(加任务,将任务新建线程,并启动线程来执行)
3.任务执行
4.获取其他任务 (获取阻塞队列里的任务task)
5.执行完task后的后续处理
总结
上面我们已经非常详细的讲解了线程池的方方面面,对于线程的使用,注意事项乃至原理,应该都有相当深刻的了解了,如果你有什么补充和意见,也欢迎评论区留下你的想法
请
登录
后发表内容
关 注
相关文章
初始c语言——————青铜的进阶之路
小游戏——————三子棋(保姆级教学)
热门文章
支付宝开发者日·厦门站
【获奖名单公布】工具类小程序话题讨论,你中奖了吗?
报名开启丨邀你一起探索云端 ai 新兴技术和发展模式
社区每周丨ide 3.7.13 beta 版上线及产品面对面第三期即将开播(8.21-8.25)
有奖捉虫,小程序云文档提升计划开始啦📢📢
热门问答
影视创作剪辑怎么提供资质
支付宝商家粉丝群
我的小程序上架三天被判违规,直接被下架了
2023/09/17(至今3天没人解决) 当面付 统一收单线下交易预创建接口 官方php easysdk验签语法错误
请问下这个是什么错误?“tracert_error,当前页面尚未配置 spmb,请参考以下文章进行配置”
您的社区活跃积分 3,登录后即可领取
网站地图