juc基础——线程池-ag九游会j9官方网站

ag九游会j9官方网站-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(分治-任务窃取)
但是使用预置的方式创建线程池并非没有弊端,比如
newfixedthreadpoolnewsinglethreadexecutor,主要问题是使用的无界队列,堆积的请求处理队列可能会耗费非常大的内存,甚至oom。 newcachedthreadpoolnewscheduledthreadpool设置的线程数最大数是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变量里
2worker创建完成以后调用runworker方法;
3runworker方法里面先把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后的后续处理
总结
上面我们已经非常详细的讲解了线程池的方方面面,对于线程的使用,注意事项乃至原理,应该都有相当深刻的了解了,如果你有什么补充和意见,也欢迎评论区留下你的想法
avatarname
后发表内容
您的社区活跃积分 3,登录后即可领取  
网站地图