并发编程-线程池ThreadPoolExecutor底层原理分析(一)

问题:

线程池的核心线程数、最大线程数该如何设置?
线程池执行任务的具体流程是怎样的?
线程池的五种状态是如何流转的?
线程池中的线程是如何关闭的?
线程池为什么一定得是阻塞队列?
线程发生异常,会被移出线程池吗?
Tomcat是如何自定义线程池的?

线程池执行任务的具体流程是怎样的?

ThreadPoolExecutor中提供了两种执行任务的方法:

1.void execute(Runnable command)

2.Future<?> submit(Runnable task)

实际上submit中最终还是调用的execute()方法,只不过会返回一个Future对象,用来获取任务执行结果:
public Future<?> submit(Runnable task) {if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null); 3execute(ftask);return ftask;}
execute(Runnable command)方法执行时会分为三步:
注意:提交一个Runnable时,不管当前线程池中的线程是否空闲,只要数量小于核心线程数就会创建 新线程。
注意:ThreadPoolExecutor相当于是非公平的,比如队列满了之后提交的Runnable可能会比正在 排队的Runnable先执行。

线程池的五种状态是如何流转的?

线程池有五种状态:
RUNNING: 接收新任务并且 处理队列中的任务
SHUTDOWN: 不会 接收新任务并且 处理队列中的任务
STOP: 不会 接收新任务并且 不会 处理队列中的任务,并且会中断在处理的任务 (注意:一个任务能不能被中断得看任务本身)
TIDYING: 所有任务都终止了,线程池中也没有线程了 ,这样线程池的状态就会转为TIDYING,一旦达到此状 态,就会调用线程池的terminated()
TERMINATED: terminated()执行完之后就会转变为TERMINATED
这五种状态并不能任意转换,只会有以下几种转换情况:
1. RUNNING -> SHUTDOWN:手动调用shutdown()触发,或者线程池对象GC时会调用finalize()从而调用 shutdown()
2. (RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()触发,如果先调shutdown()紧着调
shutdownNow(),就会发生SHUTDOWN -> STOP
3. SHUTDOWN -> TIDYING 队列为空 并且 线程池中没有线程时 自动转换
4. STOP -> TIDYING 线程池中没有线程时 自动转换(队列中可能还有任务)
5. TIDYING -> TERMINATED:terminated()执行完后就会自动转换

线程池中的线程是如何关闭的?

我们一般会使用thread.start()方法来开启一个线程,那如何停掉一个线程呢?
Thread类提供了一个stop(),但是标记了@Deprecated,为什么不推荐用stop()方法来停掉线程呢?
        因为stop()方法太粗暴了,一旦调用了stop(),就 会直接停掉线程 但是调用的时候根本不知道线程刚刚在做什么,任务做到哪一步了,这是很危险的
这里强调一点, stop()会释放线程占用的synchronized锁 (不会自动释放ReentrantLock锁,这也是 不建议用stop()的一个因素)。
public class ThreadTest {static int count = 0;static final Object lock = new Object();static final ReentrantLock reentrantLock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(new Runnable() {public void run() {// synchronized (lock) {reentrantLock.lock();for (int i = 0; i < 100; i++) {count++;try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}// }reentrantLock.unlock();}});thread.start();Thread.sleep(5 * 1000);thread.stop();//// Thread.sleep(5*1000);reentrantLock.lock();System.out.println(count);reentrantLock.unlock();// synchronized (lock) {// System.out.println(count);// }}
}
所以,我们建议通过自定义一个变量,或者通过中断来停掉一个线程,比如:
public class ThreadTest {static int count = 0;static boolean stop = false;public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(new Runnable() {public void run() {for (int i = 0; i < 100; i++) {if (stop) {break;}count++;try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}});thread.start();Thread.sleep(5 * 1000);stop = true;Thread.sleep(5 * 1000);System.out.println(count);}
}
不同点在于,当我们把stop设置为true时,线程自身可以控制到底要不要停止,何时停止,同样,我 们可以调用thread的interrupt()来中断线程:
public class ThreadTest {static int count = 0;static boolean stop = false;public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(new Runnable() {public void run() {for (int i = 0; i < 100; i++) {if (Thread.currentThread().isInterrupted()) {break;}count++;try {Thread.sleep(1000);} catch (InterruptedException e) {break;}}}});thread.start();Thread.sleep(5 * 1000);thread.interrupt();Thread.sleep(5 * 1000);System.out.println(count);}
}
 不同的地方在于,线程sleep过程中如果被中断了会接收到异常。
讲了这么多,其实线程池中就是通过interrupt()来停止线程的,比如shutdownNow()方法中会调用:
 void interruptIfStarted() {Thread t;if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {try {t.interrupt();} catch (SecurityException ignore) {}}}

线程池为什么一定得是阻塞队列?

        线程池中的线程在运行过程中,执行完创建线程时绑定的第一个任务后,就会不断的从队列中获取任 务并执行,那么如果队列中没有任务了,线程为了不自然消亡,就会阻塞在获取队列任务时,等着队列中有任务过来就会拿到任务从而去执行任务。通过这种方法能最终确保,线程池中能保留指定个数的核心线程数,关键代码为:
         try {Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();if (r != null)return r;timedOut = true;} catch (InterruptedException retry) {timedOut = false;}
某个线程在从队列获取任务时,会判断是否使用超时阻塞获取,我们可以认为非核心线程会poll(),核 心线程会take(),非核心线程超过时间还没获取到任务后面就会自然消亡了。

线程发生异常,会被移出线程池吗?

答案: 是会的, 那有没有可能核心线程数在执行任务时都出错了,导致所有核心线程都被移出了线程 池?
         在源码中,当执行任务时出现异常时,最终会执行processWorkerExit(),执行完这个方法后,当前线 程也就自然消亡了,但是!processWorkerExit()方法中会额外再新增一个线程,这样就能维持住固定 的核心线程数。

Tomcat是如何自定义线程池的?

Tomcat中用的线程池为org.apache.tomcat.util.threads.ThreadPoolExecutor,注意类名和JUC下的
一样,但是包名不一样。

Tomcat会创建这个线程池:

  public void createExecutor() {internalExecutor = true;TaskQueue taskqueue = new TaskQueue();TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon,getThreadPriority());executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60,TimeUnit.SECONDS, taskqueue, tf);taskqueue.setParent((ThreadPoolExecutor) executor);}

 //注入传入的队列为TaskQueue,它的入队逻辑为:

 public boolean offer(Runnable o) {//we can't do any checksif (parent == null) {return super.offer(o);}//we are maxed out on threads, simply queue the objectif (parent.getPoolSize() == parent.getMaximumPoolSize()) {return super.offer(o);}//we have idle threads, just add it to the queueif (parent.getSubmittedCount() <= (parent.getPoolSize())) {return super.offer(o);}//if we have less threads than maximum force creation of a new threadif (parent.getPoolSize() < parent.getMaximumPoolSize()) {return false;}//if we reached here, we need to add it to the queuereturn super.offer(o);}
特殊在:
        入队时,如果线程池的线程个数等于最大线程池数才入队。
        入队时,如果线程池的线程个数小于最大线程池数,会返回false,表示入队失败。
这样就控制了,Tomcat的这个线程池,在提交任务时:
        1. 仍然会先判断线程个数是否小于核心线程数,如果小于则创建线程
        2. 如果等于核心线程数,会入队,但是线程个数小于最大线程数会入队失败,从而会去创建线程。
 所以随着任务的提交,会优先创建线程,直到线程个数等于最大线程数才会入队。
当然其中有一个比较细的逻辑是:在提交任务时,如果正在处理的任务数小于线程池中的线程个数, 那么也会直接入队,而不会去创建线程,也就是上面源码中getSubmittedCount的作用。

线程池的核心线程数、最大线程数该如何设置?

我们都知道,线程池中有两个非常重要的参数:
1. corePoolSize: 核心线程数,表示线程池中的常驻线程的个数
2. maximumPoolSize: 最大线程数,表示线程池中能开辟的最大线程个数

那这两个参数该如何设置呢?

我们对线程池负责执行的任务分为三种情况:
         1. CPU密集型任务,比如找出1-1000000中的素数
        2. IO密集型任务,比如文件IO、网络IO
        3. 混合型任务
CPU密集型任务的特点时,线程在执行任务时会一直利用CPU,所以对于这种情况,就尽可能避免发 生线程上下文切换。
        比如,现在我的电脑只有一个CPU,如果有两个线程在同时执行找素数的任务,那么这个CPU就需要 额外的进行线程上下文切换,从而达到线程并行的效果,此时执行这两个任务的总时间为:
         任务执行时间*2+线程上下文切换的时间
而如果只有一个线程,这个线程来执行两个任务,那么时间为:
         任务执行时间*2
所以对于CPU密集型任务,线程数最好就等于CPU核心数,可以通过以下API拿到你电脑的核心数:
 Runtime.getRuntime().availableProcessors()
只不过,为了应对线程执行过程发生缺页中断或其他异常导致线程阻塞的请求,我们可以额外在多设 置一个线程,这样当某个线程暂时不需要CPU时,可以有替补线程来继续利用CPU。
所以,对于CPU密集型任务,我们可以设置线程数为: CPU核心数+1
我们在来看IO型任务,线程在执行IO型任务时,可能大部分时间都阻塞在IO上,假如现在有10个
CPU,如果我们只设置了10个线程来执行IO型任务,那么很有可能这10个线程都阻塞在了IO上,这样 这10个CPU就都没活干了,所以,对于IO型任务,我们通常会设置线程数为: 2*CPU核心数
不过,就算是设置为了 2*CPU核心数 ,也不一定是最佳的,比如,有10个CPU,线程数为20,那么也 有可能这20个线程同时阻塞在了IO上,所以可以再增加线程,从而去压榨CPU的利用率。
通常,如果IO型任务执行的时间越长,那么同时阻塞在IO上的线程就可能越多,我们就可以设置更多 的线程,但是,线程肯定不是越多越好 ,我们可以通过以下这个公式来进行计算:
         线程数 = CPU核心数 *( 1 + 线程等待时间 / 线程运行总时间 )
线程等待时间: 指的就是线程没有使用CPU的时间,比如阻塞在了IO
线程运行总时间: 指的是线程执行完某个任务的总时间
我们可以利用jvisualvm抽样来估计这两个时间:
图中表示,在刚刚这次抽样过程中,run()总共的执行时间为538948ms,利用了CPU的时间为
86873ms,所以没有利用CPU的时间为538948ms-86873ms。
所以我们可以计算出:
         线程等待时间 = 538948ms-86873ms
        线程运行总时间 = 538948ms
所以: 线程数 = 8 *( 1 + (538948ms-86873ms) / 538948ms )= 14.xxx所以根据公式算出来的线程为14、15个线程左右。
按上述公式,如果我们执行的任务IO密集型任务,那么:线程等待时间 = 线程运行总时间,所以:
线程数 = CPU核心数 *( 1 + 线程等待时间 / 线程运行总时间 )
= CPU核心数 *( 1 + 1 )
= CPU核心数 * 2
以上只是理论,实际工作中情况会更复杂,比如一个应用中,可能有多个线程池,除开线程池中的线 程可能还有很多其他线程,或者除开这个应用还是一些其他应用也在运行,所以实际工作中如果要确 定线程数,最好是压测。
比如写一个:
@RestControllerpublic class ZhouyuController {@GetMapping("/test")public String test() throws InterruptedException {Thread.sleep(1000);return "zhouyu";}}
这个接口会执行1s,我现在利用apipost来压:
这是在Tomcat默认最大200个线程的请求下的压测结果。
当我们把线程数调整为500:
server.tomcat.threads.max=500

发现执行效率提高了一倍,假如再增加线程数到1000:
性能就降低了。

总结,我们在工作中,对于:

1. CPU密集型任务: CPU核心数+1,这样既能充分利用CPU,也不至于有太多的上下文切换成本
2. IO型任务 :建议压测,或者先用公式计算出一个理论值(理论值通常都比较小)
3. 对于核心业务(访问频率高): 可以把核心线程数设置为我们压测出来的结果,最大线程数可以等于核心线程 数,或者大一点点,比如我们压测时可能会发现500个线程最佳,但是600个线程时也还行,此时600就可以为最 大线程数
4. 对于非核心业务(访问频率不高) ,核心线程数可以比较小,避免操作系统去维护不必要的线程,最大线程数可 以设置为我们计算或压测出来的结果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/167010.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

优维低代码实践:片段

优维低代码技术专栏&#xff0c;是一个全新的、技术为主的专栏&#xff0c;由优维技术委员会成员执笔&#xff0c;基于优维7年低代码技术研发及运维成果&#xff0c;主要介绍低代码相关的技术原理及架构逻辑&#xff0c;目的是给广大运维人提供一个技术交流与学习的平台。 优维…

【vue】使用less报错:显示this.getOptions is not a function

在vue-cli中使用 lang“less” 时报错&#xff1a; Module build failed: TypeError: this.getOptions is not a function at Object.lessLoader 原因&#xff1a;版本过高所致&#xff0c;所用版本为 解决&#xff1a;降低版本&#xff1a;npm install less-loader4.1.0 --s…

STM32+摁键与定时器实现Led灯控制(中断)

中断作为单片机开发必须掌握的内容&#xff0c;它能够在不搭载操作系统的情况下让我们体验多任务处理的快感&#xff0c;保证了高优先级任务的实时性&#xff0c;同时系统中断也能够提供给用户在核心发生错误之后进行处理的机会。STM32F103系列单片机中断非常强大&#xff0c;每…

Linux中常见的权限问题

目录 前言1. 目录权限2. umask3. 粘滞位结语 前言 在了解完上一篇文章 Linux权限的理解与操作 之后&#xff0c;还有一些比较常见的权限问题需要我们去了解。其中包括目录的权限&#xff0c;umask 以及 粘滞位的使用。 1. 目录权限 问题一&#xff1a;进入一个目录&#xff0…

QT QGLWidge

QGLWidget 学习 前言1.四边形 QGLWidget 2*32. 正方体 1*2前言 1.四边形 QGLWidget 2*3 坐标 效果 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存glLoadIdentity(); //重置当前的模型观察矩阵glTranslate…

2001-2022年全国290+个地级市高铁开通数据

2001-2022年全国290个地级市高铁开通数据 1、时间&#xff1a;2001-2022年 2、范围&#xff1a;298地级市&#xff08;293地级市数&#xff08;其中莱芜市2019年撤市设区&#xff09;4直辖市数 &#xff09; 3、来源&#xff1a;国家铁路局、铁路客货运输专刊及相关统计 国…

LNMP架构部署Discuz论坛系统

文章目录 LNMP架构&部署Discuz论坛系统部署LNMP架构环境前期准备安装Nginx安装mariadb安装php配置nginx 部署Discuz论坛系统下载Discuz论坛系统代码包部署Discuz论坛系统配置虚拟主机安装Discuz论坛访问站点尝试注册一个账号 LNMP架构&部署Discuz论坛系统 部署LNMP架构…

深度学习 | Pytorch深度学习实践 (Chapter 10、11 CNN)

十、CNN 卷积神经网络 基础篇 首先引入 —— 二维卷积&#xff1a;卷积层保留原空间信息关键&#xff1a;判断输入输出的维度大小特征提取&#xff1a;卷积层、下采样分类器&#xff1a;全连接 引例&#xff1a;RGB图像&#xff08;栅格图像&#xff09; 首先&#xff0c;老师…

Redis常见问题的解决方案(缓存穿透/缓存击穿/缓存雪崩/数据库缓存数据不一致)

Redis解决缓存数据库不一致的方案 用 先 操作数据库 再 操作缓存 的策略来实现缓存数据库数据一致具体做法是 更新数据库数据然后删除缓存 虽然还是会有线程安全问题 比如 假设此时缓存刚好失效了 线程1 查询缓存失败 从数据库读取了旧数据 还没写入缓存的时候 被调度到 线程…

C++-json(2)-unsigned char-unsigned char*-memcpy-strcpy-sizeof-strlen

1.类型转换&#xff1a; //1.赋值一个不知道长度的字符串unsigned char s[] "kobe8llJfFwFSPiy"; //1.用一个字符串初始化变量 unsigned int s_length strlen((char*)s); //2.获取字符串长度//2.字符串里有双引号"" 需要…

PAM从入门到精通(十九)

接前一篇文章&#xff1a;PAM从入门到精通&#xff08;十八&#xff09; 本文参考&#xff1a; 《The Linux-PAM Application Developers Guide》 PAM 的应用开发和内部实现源码分析 先再来重温一下PAM系统架构&#xff1a; 更加形象的形式&#xff1a; 六、整体流程示例 2.…

函数栈帧的创建和销毁

目录 引言&#xff1a; 1&#xff0c;函数栈帧的概念 2&#xff0c;函数栈帧的创建与销毁过程 2.1预备知识 2.2main函数栈帧的创建 2.2.1push ebp 2.2.2mov ebp,esp 2.2.3sub esp,0E4h 2.2.4push ebx &#xff1b;push esi&#xff1b;push edi 2…

C++STL的迭代器(iterator)

一、定义 迭代器是一种检查容器内元素并且遍历容器内元素的数据类型。 【引用自&#xff1a;C迭代器&#xff08;iterator&#xff09;_c iterator_NiUoW的博客-CSDN博客】迭代器是一个变量&#xff0c;相当于容器和操纵容器的算法之间的中介。C更趋向于使用迭代器而不是数组下…

day01_matplotlib_demo

文章目录 折线图plot多个绘图区绘制数学函数图像散点图scatter柱状图bar直方图histogram饼图pie总结 折线图plot import matplotlib.pyplot as pltplt.figure(figsize(15, 6), dpi80) plt.plot([1, 0, 9], [4, 5, 6]) plt.show()### 展现一周天气温度情况 # 创建画布 plt.figu…

【C++】引用’‘的深入解析

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

C语言学习系列->动态内存管理

文章目录 前言概述&#x1f6a9;malloc and free&#x1f51c;malloc&#x1f51c;free &#x1f6a9;calloc and realloc&#x1f51c;calloc&#x1f51c;realloc 前言 要想学好数据结构&#xff0c;在C语言学习过程中就需要把指针、结构体和动态内存管理学好。在前面的文章&…

makesense在线yolov5标注

文章目录 一、创建图片文件夹和label.txt二、在线标注数据 参考文章博主&#xff1a;风吹落叶花飘荡 一、创建图片文件夹和label.txt 创建一个放置图片的文件夹images&#xff0c;存放需要标注的图片&#xff08;图片最好重命名为1,2,3…避免后面混淆&#xff09; 创建label.t…

C++标准模板(STL)- 类型支持 (运行时类型识别,type_info )

运行时类型识别 定义于头文件 <typeinfo> 含有某个类型的信息&#xff0c;由实现生成。​​这是 typeid 运算符所返回的类。 std::type_info 定义于头文件 <typeinfo> class type_info; 类 type_info 保有一个类型的实现指定信息&#xff0c;包括类型的名称和…

我单方面宣布:腾讯云服务器优惠价格(双11特价)

2023腾讯云双十一服务器优惠价格表多少钱一年&#xff1f;轻量服务器2核2G3M、2核2G4M、2核4G5M、4核8G12M、8核16G18M、16核32G28M和云服务器CVM标准型S5实例优惠价格&#xff0c;腾讯云百科今年双11服务器价格会在当前的价格基础上享受个9折优惠&#xff0c;可领券 https://c…

正则表达式之学习笔记

正则表达式学习笔记 一、概念二、正则表达式组成三、常见的正则表达式3.1 .匹配任意字符3.2 * 匹配前一个字符的0个或多个实例3.3 ^ 匹配输入字符串的开头3.4 $ 匹配行尾3.5 [] 匹配字符集合\<\> 精确匹配符号 一、概念 正则表达式是由一系列特殊字符组成的字符串&#…