线程池ThreadPoolExecutor原理

news/2024/4/26 17:40:53/文章来源:https://blog.csdn.net/qq_44027353/article/details/130015493

文章目录

  • 线程池ThreadPoolExecutor原理
    • 核心参数如何设置
      • 核心线程数和最大线程数
      • 线程空闲时间
      • 阻塞队列设置
    • 线程池的五种状态
    • 原理
      • 执行流程
      • 拒绝策略
      • 线程淘汰机制

线程池ThreadPoolExecutor原理

核心参数如何设置

核心线程数和最大线程数

线程池中线程数量我们一般要区分任务的类型,

  • 如果是cpu密集性任务那么线程数一般为cpu核数+1

    // 查看cpu核心数
    int cores = Runtime.getRuntime().availableProcessors();
    
  • 如果是IO密集型任务可以按照cpu核数 * (1 + cpu等待时长/总时长)

    cpu的等待时长其实就是除了计算之外io操作耗时,cpu等待时长和任务总时长可以通过jvisualvm工具来查看,

    cpu等待时长 = 总时间 - 总时间(CPU)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cn2jLU7r-1680856236361)(picture/并发/image-20230407120135689.png)]


当然这只是理论值,实际项目中肯定会存在多个线程池,具体还要通过压测选出较合适的线程数。


当线程数确定后,那么如何设置核心线程数和最大线程数嘞?

其实这就主要看我们要执行的任务是不是核心业务,请求是否频繁。如果是核心业务每秒都有很高的请求那么我们就可以把核心线程数和最大线程数设置一样或者相近。如果不是核心业务,几分钟或者几十分钟才来一些请求,那么核心线程数就没必要设置过大,设置最大线程数一半 或者1/3都行。



线程空闲时间

线程空闲时间没有具体的要求,一般就设置半分钟或者一分钟都行



阻塞队列设置

队列的容量设置多大,主要就是看队列中最后一个任务的等待时长业务是否能够容忍。

我们首先要计算出每个任务的执行耗时,然后再看所有核心线程数去拿队列中的最后一个任务的耗时,业务能否接收,如果能接收那么队列长度的设置就可以。

假如现在核心线程数是10个,每个任务的耗时是1s,阻塞队列的长度是100。那么队列中最后的任务就需要等待9s,然后自己再执行1s。如果业务系统能接受这个耗时那么队列长度就不用缩短。

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 100, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));



线程池的五种状态

线程池有五种状态:

  • 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()执行完后就会自动转换



原理

执行流程

  1. 创建线程池时,线程池中是不会创建线程的。除非我们自己再显示调用prestartAllCoreThreads()方法才会去创建核心线程。

  2. 刚开始线程池中是没有线程的,如果来了任务那么就直接去创建线程处理,如果核心线程处理完任务了,但是线程数量还没有达到核心线程数,此时来了任务也还是会去创建新线程处理,直到线程数量达到了核心线程数。

  3. 线程数 >= 核心线程数后,再来新任务就会直接放入阻塞队列中,并唤醒等待的线程去处理,当队列中没有任务了那么线程就阻塞。

  4. 假如队列中放满了,那么才会去创建新的线程去处理任务。

  5. 如果新创建的线程达到了最大线程数量那么就会触发拒绝策略


我们直接看线程池执行任务的源码:

public void execute(Runnable command) {if (command == null)throw new NullPointerException();int c = ctl.get();// 首先看当前线程池数量是否小于核心线程数,如果小于则直接调用addWorker()方法 创建新线程// 这里的addWorker()方法最后一个参数是trueif (workerCountOf(c) < corePoolSize) {if (addWorker(command, true))return;c = ctl.get();}// 核心线程数量达到后,就会调用offer()方法入队if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get();if (! isRunning(recheck) && remove(command))reject(command);else if (workerCountOf(recheck) == 0)addWorker(null, false);}// 如果队列满了 入队失败  那么就会调用addWorker()再去创建新线程处理任务// 这里的addWorker()方法最后一个参数是false。在addWorker()方法中最后一个boolean的传参就是判断比较核心线程数还是最大线程数  // 当前线程池工作线程数量 >= (boolean ? corePoolSize : maximumPoolSize)else if (!addWorker(command, false))// 如果达到了最大线程数则触发拒绝策略reject(command);
}

而Tomcat中的线程池对整体流程做了一些改动:

  • 创建线程池时就会初始化核心线程
  • 来任务后先使用核心线程,核心线程处理不过来时就创建新线程,当达到最大线程数时再入队



拒绝策略

线程池中有四种拒绝策略

  • AbortPolicy:抛异常,也是默认的拒绝策略
  • CallerRunsPolicy:哪里来的回哪里,比如回main线程。
  • DiscardOldestPolicy:满了,会尝试丢弃队列头部第一个,如果第一个没结束,会抛弃任务,但是也不会抛异常。
  • DiscardPolicy:会抛弃任务,但是不抛异常。



线程淘汰机制

在这里插入图片描述


线程池中的淘汰机制有三种:

  • 线程池非核心线程空闲时间超过设定的最大空闲时间
  • 执行任务时出异常
  • 线程池调用了shutdown()/shutdownNow()方法

三种线程淘汰策略接下来就根据源码分析

这里简单介绍一下核心源码,首先从上面的代码中我们可以知道创建线程执行任务是调用的addWorker()方法

private boolean addWorker(Runnable firstTask, boolean core) {......boolean workerStarted = false;boolean workerAdded = false;Worker w = null;try {// 这里会把我们传入了runnable对象封装为一个Worker对象,Worker也实现了Runnable接口 也是一个线程类w = new Worker(firstTask);// 取出线程 变量tfinal Thread t = w.thread;if (t != null) {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {int rs = runStateOf(ctl.get());if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {if (t.isAlive()) // precheck that t is startablethrow new IllegalThreadStateException();workers.add(w);int s = workers.size();if (s > largestPoolSize)largestPoolSize = s;workerAdded = true;}} finally {mainLock.unlock();}if (workerAdded) {// 启动线程t.start();workerStarted = true;}}} finally {if (! workerStarted)addWorkerFailed(w);}return workerStarted;
}

看看Worker类的源码

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {private static final long serialVersionUID = 6138294804551838833L;/** 上面addWorker()方法中获取的就是这个属性的值 */final Thread thread;/** 这个属性就是我们自定义要执行的任务 */Runnable firstTask;volatile long completedTasks;Worker(Runnable firstTask) {setState(-1); // AQS的statethis.firstTask = firstTask; // 把我们真正要执行的任务赋值给了firstTask属性this.thread = getThreadFactory().newThread(this); // 这里创建了一个新线程,传参是this 也就是当前对象实例}// 所以addWorker()方法中启动线程,实际上会调用这个run()方法public void run() {runWorker(this);}...
}

接下来我们再看看runWorker(this);方法

final void runWorker(Worker w) {Thread wt = Thread.currentThread();// 这里把我们真正要执行的任务取出来Runnable task = w.firstTask;w.firstTask = null;w.unlock(); boolean completedAbruptly = true;try {// 这是一个循环,表示线程不断的从阻塞队列中拿任务,如果没有任务就阻塞,如果有任务就继续执行任务// 所以,如果getTask()方法返回了一个null,那么也就表示当前线程对象的run()方法要执行完了,那么这个线程对象也就没了while (task != null || (task = getTask()) != null) {w.lock();if ((runStateAtLeast(ctl.get(), STOP) ||(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP))) &&!wt.isInterrupted())wt.interrupt();try {beforeExecute(wt, task);Throwable thrown = null;try {// 这里运行任务,可能会出异常task.run();} catch (RuntimeException x) {thrown = x; throw x;} catch (Error x) {thrown = x; throw x;} catch (Throwable x) {thrown = x; throw new Error(x);} finally {afterExecute(task, thrown);}} finally {task = null;w.completedTasks++;w.unlock();}}// 如果出异常 这行代码也不会执行,值还是true,下面方法会用到这个变量completedAbruptly = false;} finally {// 如果线程执行任务时抛异常了,那么就会跳出上方的循环,进入到这里,这个方法就是会再创建一个新的线程补进线程池中processWorkerExit(w, completedAbruptly);}
}

我们再看看看 getTask()从阻塞队列中获取任务的方式

private Runnable getTask() {// 标识当前线程是否空闲超过指定时间boolean timedOut = false; // 这是一个死循环for (;;) {int c = ctl.get();int rs = runStateOf(c);// 线程池的状态,如果线程池调用了shutdown()/shutdownNow()方法,那么这里就会满足 然后返回null。// 接着调用getTask()方法的循环也就会跳出,线程池中的线程run()方法也就都执行完了 线程也就都释放了// 不同点是shutdown()是把状态改为SHUTDOWN,也就是说会把队列中的任务执行完才会释放线程。而shutdownNow()是把状态改为STOP,这里就直接满足了if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {decrementWorkerCount();return null;}int wc = workerCountOf(c);// 这里的timed是判断当前工作线程是否大于了核心线程数boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;// 刚开始的循环 timedOut是为false 下面这个if不会满足。  如果工作线程小于了核心线程下面if也不满足// 如果经过下面代码的逻辑 timeOut为true后,那么就会CAS改变当前工作线程 数量-1  因为是CAS操作 只有一个线程会成功,// 返回null,调用getTask()方法的循环也就会跳出,线程池中的线程run()方法也就都执行完了 线程也就都释放了if (  (wc > maximumPoolSize || (timed && timedOut))  &&  (wc > 1 || workQueue.isEmpty())  ) {if (compareAndDecrementWorkerCount(c))return null;continue;}try {// timed是判断当前工作线程是否大于了核心线程数,如果大于了那么线程就阻塞特定超时时长,时间达到后如果队列中没有任务这里的r=null// 如果小于核心线程数,那么就一直阻塞,直到生产者唤醒// 阻塞队列中,消费者阻塞后,生产者生产后是调用的signal() 而不是signalAll()Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();// 拿到了任务就返回if (r != null)return r;// 没有拿到任务就将下面变量置为true,重新进行一次循环timedOut = true;} catch (InterruptedException retry) {timedOut = false;}}
}

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

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

相关文章

操作技巧 | Revit中如何新建系统类型并赋予颜色?

大家好&#xff0c;这里是行走的安利机---建模助手。 新建系统后&#xff0c;把材质赋予系统&#xff0c;以做出不同颜色的管道和风管系统&#xff0c;那么&#xff1a;Revit中如何新建系统类型并赋予颜色呢&#xff1f; 下面小编说下解决方案。 REVIT 具体解决办法如下 正…

携多款产品亮相“深圳先进制造业集群展”,华秋积极探索发展机遇

4月7日&#xff0c;在深圳市工业和信息化局指导下&#xff0c;由深圳先进技术研究院作为总促进机构的深圳市新一代信息通信产业集群于第十一届中国电子信息博览会&#xff08;CITE2023&#xff09;期间举办 “深圳先进制造业集群展”。 本次先进制造业集群展以“科技带动产业创…

设计干货:PCB为什么要拼版?PCB拼版的适用方式分享

PCB为什么要拼版&#xff1f; 拼版主要是为了满足 生产的需求 &#xff0c;有些PCB板太小&#xff0c;不满足做夹具的要求&#xff0c;所以需要拼在一起进行生产。 拼版也可以提高SMT贴片的 焊接效率 &#xff0c;如只需要过一次SMT&#xff0c;即可完成多块PCB的焊接。 同时…

FPGA纯verilog实现UDP通信,三速网自协商仲裁,动态ARP和Ping功能,提供工程源码和技术支持

目录1、前言2、我这里已有的UDP方案3、UDP详细设计方案MAC层发送MAC发送模式ARP发送IP层发送IP发送模式UDP发送MAC层接收ARP接收IP层接收UDP接收SMI读写控制SMI配置10/100/1000M仲裁ICMP应答 (ping)ARP缓存CRC校验以太网测试模块RGMII转GMII模块4、vivado工程详解5、上板调试验…

《大众金融》企业级开发实战

目录 主要内容 1 配置中心简介 1.1 什么是配置 1.2 传统配置形式存在的问题 1.3 配置中心的作用 2 Apollo简介 2.3 Apollo特性 2.4 产品对比 2.5 Apollo初体验 2.5.1 访问控制台 应用配置中心Apollo-讲义 主要内容 1&#xff09;了解配置中心的概念以及使用场景 2&…

单元测试系列 | 如何更好地测试依赖外部接口的方法

背景 在现在这个微服务时代&#xff0c;我们项目中经常都会遇到很多业务逻辑是依赖其他服务或者第三方接口。工作中各位同学对于这类型场景的测试方式也是五花八门&#xff0c;有些是直接构建一个外部mock服务&#xff0c;返回一些固定的response;有些是单元测试都不写&#x…

Linux复习 / 命令与权限部分QA梳理

文章目录前言Q&AshellQ&#xff1a;什么是shell&#xff1f;Q&#xff1a;shell的作用&#xff1f;Q&#xff1a;为什么要有shell&#xff1f;Q&#xff1a;shell的生命周期多长&#xff1f;Q&#xff1a;shell的原理/实现是怎样的&#xff1f;Q&#xff1a;为什么会有内建…

Scrum Master 应该采取哪些措施来提高团队效率?

项目经理应该从这5方面提高团队的开发效率 1、目标明确有时间节点 提高团队开发效率&#xff0c;最重要的是明确目标与期限。制定SMART目标&#xff0c;明确告知成员要实现什么&#xff0c;输出什么&#xff0c;标准以及时限等&#xff0c;需要考虑目标的可达成性和目标与项目的…

【牛客刷题专栏】0x17:JZ17打印从1到最大的n位数(C语言编程题)

前言 个人推荐在牛客网刷题(点击可以跳转)&#xff0c;它登陆后会保存刷题记录进度&#xff0c;重新登录时写过的题目代码不会丢失。个人刷题练习系列专栏&#xff1a;个人CSDN牛客刷题专栏。 题目来自&#xff1a;牛客/题库 / 在线编程 / 剑指offer&#xff1a; 目录前言问题…

Java初阶(异常)

文章目录一、异常的结构体系二、异常的处理2.1 防御式编程2.2 异常的抛出2.4 异常的捕获&#xff08;异常的具体处理方式&#xff09;&#xff08;1&#xff09;异常声明 throws&#xff08;2&#xff09; 捕获处理 try-catch2.4 异常的处理流程三、自定义异常类一、异常的结构…

go学习线路图

1. go学习线路图 1.1.2. 资源 先决条件 GoSQL 通用开发技能 学习 GIT&#xff0c;在 GitHub 上建立一些仓库&#xff0c;与其它人分享你的代码了解 HTTP(S) 协议&#xff0c;request 方法&#xff08;GET, POST, PUT, PATCH, DELETE, OPTIONS&#xff09;不要害怕使用 Google&a…

和数软件荣获上海市“专精特新”企业荣誉认定

近日&#xff0c;上海市经济和信息化委员会公示了2022年上海市“专精特新”企业名单。根据《关于组织开展2022年创新型中小企业评价、专精特新中小企业认定和复核工作的通知》&#xff08;沪经信企〔2022〕776号&#xff09;&#xff0c;经专家评审和综合评估&#xff0c;上海和…

学会吊打面试官之map

小白&#xff1a;大牛&#xff0c;我最近学习了一些C的STL容器&#xff0c;但是我还是有一些疑惑&#xff0c;特别是对于map&#xff0c;我不太理解它的底层实现和具体用法。能否跟我讲一下&#xff1f; 大牛&#xff1a;当然可以啊&#xff0c;map是一种非常常用的关联式容器…

小企业选择什么样的CRM系统比较合适,有什么特点?

CRM客户管理系统已经成为各种规模的企业&#xff0c;特别是小型企业的重要工具。CRM系统帮助小型企业更有效地管理客户数据和互动&#xff0c;简化销售流程&#xff0c;并提高客户满意度。市场上有如此多的选择&#xff0c;小企业该如何选择合适的CRM系统&#xff1f; 什么是C…

深圳CPDA|如何着手商业数据分析?

商业数据分析是一项非常重要的工作&#xff0c;可以帮助企业做出更明智的决策。 下面是一些着手商业数据分析的步骤&#xff1a; 1.确定你的问题 首先需要明确你想要解决什么问题。 这通常需要与业务团队沟通&#xff0c;以便了解他们正在寻找哪些信息。 2.收集数据 收集数…

linux语言学习记录

文章目录前言一、linux文件结构二、指令三、Gvim编辑器1、命令模式2、底行命令四、正则表达式1、表达式匹配举例2、对文件里面内容进行操作3、使用 \( 和 )\ 符号括起正规表达式&#xff0c;即可在后面使用\1和\2等变量来访问和中的内容前言 记录自己学习linux的笔记&#xff…

IFPUG功能点度量5:计算功能规模

功能点计数类型&#xff1a;开发项目、升级项目、应用 一、 三种功能能规模计算&#xff1a; 1、开发项目计算 DFP&#xff08;开发项目功能规模&#xff09;ADD&#xff08;交付用户的功能规模&#xff09;CFP&#xff08;转换功能的功能规模&#xff09; 2、升级项目计算 …

【代码笔记】Pytorch学习 DataLoader模块详解

Pytorch DataLoader模块详解dataloader整体结构DataLoaderinit 初始化参数解释代码解析IterableDataset 判断构建Sampler&#xff0c;单样本构建BatchSampler&#xff0c;组建batch构建collate_fn 对获取的batch进行处理其他的一些逻辑判断_get_iterator代码解析multiprocessin…

【Python】轻松掌握基础语法(一)

文章目录常量和表达式变量和类型变量的定义变量的使用变量的类型intfloatstrbool动态类型注释输入和输出输出输入运算符算数运算符关系运算符逻辑运算符赋值运算符其他常量和表达式 print(1 2 * 3)print是Python内置的一个函数&#xff0c;作用为输入打印到控制台形如1 2 * …

人工智能前沿——「全域全知全能」人类新宇宙ChatGPT

&#x1f680;&#x1f680;&#x1f680;OpenAI聊天机器人ChatGPT——「全域全知全能」人类全宇宙大爆炸&#xff01;&#xff01;&#x1f525;&#x1f525;&#x1f525; 一、什么是ChatGPT?&#x1f340;&#x1f340; ChatGPT是生成型预训练变换模型&#xff08;Chat G…