线程池实现“线程复用”的原理

news2024/11/15 9:30:08

线程池实现“线程复用”的原理

学习线程复用的原理,以及对线程池的 execute 这个非常重要的方法进行源码解析。

线程复用原理

我们知道线程池会使用固定数量或可变数量的线程来执行任务,但无论是固定数量或可变数量的线程,其线程数量都远远小于任务数量,面对这种情况线程池可以通过线程复用让同一个线程去执行不同的任务,那么线程复用背后的原理是什么呢?

线程池可以把线程和任务进行解耦,线程归线程,任务归任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。在线程池中,同一个线程可以从 BlockingQueue 中不断提取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中,不停地检查是否还有任务等待被执行,如果有则直接去执行这个任务,也就是调用任务的 run 方法,把 run 方法当作和普通方法一样的地位去调用,相当于把每个任务的 run() 方法串联了起来,所以线程数量并不增加。

我们首先来复习一下线程池创建新线程的时机和规则:

如流程图所示,当提交任务后,线程池首先会检查当前线程数,如果此时线程数小于核心线程数,比如最开始线程数为0,则新建线程并执行任务,随着任务的不断增加,线程数会逐渐增加并达到核心线程数。此时如果任有任务被不断提交,就会被放入waitQueue任务队列中,等待核心线程执行完当前任务后重新从workQueue中提取正在等待被执行的任务。

此时,假设我们的任务特别多,已经达到了workQueue的容量上限,这时线程池就会启动后备力量,也就是maxPoolSize,线程池会在corePoolSize核心线程数的基础上继续创建线程来执行任务,假设任务被不断提交,线程池会持续创建线程知道线程数达到maxPoolSize最大线程数,如果依然有任务提交,这就超过了线程池的最大处理能力,这个时候线程池就会拒绝这些任务,我们可以看到实际上任务进来之后,线程池会逐一判断corePoolSize、workQueue、maxPoolSize,如果依然不能满足需求,则会拒绝任务。

我们接下来具体看看代码是如何实现的,我们从execute方法开始分析,源码如下所示:

上面这段代码是Java线程池中 execute方法的实现部分。

  1. 首先会检查当前线程池的状态,包括正在运行的线程数量和线程池状态等信息。
  2. 如果正在运行的线程数量小于核心线程数 corePoolSize,那么会尝试启动一个新的线程来执行任务。这里使用 addWorker 方法来启动新线程,并将任务传递给它执行。
  3. 如果任务能够成功地加入到任务队列中(这里使用了 workQueue.offer(command)),那么会进行进一步检查:
    • 再次检查线程池的运行状态,确保没有在方法开始时线程池已经关闭。
    • 如果发现之前有线程死亡或线程池已经关闭,就会回滚将任务加入到任务队列的操作。
    • 如果发现当前线程池中没有运行的线程,会尝试添加一个新的线程来执行任务。
  4. 如果任务无法加入到任务队列中,会再次尝试添加一个新的线程来执行任务。

先有个整体把握,接下来逐行分析。首先看下前几行:

//如果传入的Runnable的空,就抛出异常
if (command == null) 
    throw new NullPointerException();

execute 方法中通过 if 语句判断 command ,也就是 Runnable 任务是否等于 null,如果为 null 就抛出异常。

接下来判断当前线程数是否小于核心线程数,如果小于核心线程数就调用 addWorker() 方法增加一个 Worker,这里的 Worker 就可以理解为一个线程:

if (workerCountOf(c) < corePoolSize) { 
    if (addWorker(command, true)) 
        return;
    c = ctl.get();
}

那 addWorker 方法又是做什么用的呢?addWorker 方法的主要作用是在线程池中创建一个线程并执行第一个参数传入的任务,它的第二个参数是个布尔值,如果布尔值传入 true 代表增加线程时判断当前线程是否少于 corePoolSize,小于则增加新线程,大于等于则不增加;同理,如果传入 false 代表增加线程时判断当前线程是否少于 maxPoolSize,小于则增加新线程,大于等于则不增加,所以这里的布尔值的含义是以核心线程数为界限还是以最大线程数为界限进行是否新增线程的判断。addWorker() 方法如果返回 true 代表添加成功,如果返回 false 代表添加失败。

我们接下里看下一部分代码:

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失败了,那么就需要通过if (isRunning(c) && workQueue.offer(command))检查线程池状态是否为running,如果线程池状态是running就把任务放入任务队列中,也就是workQueue.offer(command)。如果线程池已经不处于running状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,并执行拒绝策略,代码如下所示:

if (! isRunning(recheck) && remove(command)) 
    reject(command);

下面我们再来看后一个else分支:

else if (workerCountOf(recheck) == 0) 
    addWorker(null, false);

能进入这个else说明前面判断到线程池状态为running,那么当任务被添加进来之后就需要防止没有可执行线程的情况发生(比如之前的线程被回收了或者意外终止了),所以此时如果检查当前线程数为0,也就是workerCountOf(recheck == 0),那就执行addWorker()方法新建线程。

我们再来看最后一部分代码:

else if (!addWorker(command, false)) 
    reject(command);

执行到这里,说明线程池不是running状态或线程数大于或等于核心线程数并且任务队列已经满了,根据规则,此时需要添加新线程,直到线程数达到“最大线程数”,所以此时就会再次调用addWorker()方法并将第二个参数传入false,传入false代表增加

线程时判断当前线程是否少于maxPoolSize,小于则增加新线程,大于等于则不增加,也就是以maxPoolSize为上限创建新的worker。

addWorker 方法如果返回 true 代表添加成功,如果返回 false 代表任务添加失败,说明当前线程数已经达到 maxPoolSize,然后执行拒绝策略 reject 方法。如果执行到这里线程池的状态不是 Running,那么 addWorker 会失败并返回 false,所以也会执行拒绝策略 reject 方法。

可以看出,在 execute 方法中,多次调用 addWorker 方法把任务传入,addWorker 方法会添加并启动一个 Worker,这里的 Worker 可以理解为是对 Thread 的包装,Worker 内部有一个 Thread 对象,它正是最终真正执行任务的线程,所以一个 Worker 就对应线程池中的一个线程,addWorker 就代表增加线程。

线程复用的逻辑实现主要在 Worker 类中的 run 方法里执行的 runWorker 方法中。

简化后的 runWorker 方法代码如下所示。

runWorker(Worker w) {
    Runnable task = w.firstTask;
    while (task != null || (task = getTask()) != null) {
        try {
            task.run();
        } finally {
            task = null;
        }
    }
}

可以看出,实现线程复用的逻辑主要在一个不停循环的 while 循环体中。

  1. 通过取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中获取待执行的任务。
  2. 直接调用 task 的 run 方法来执行具体的任务(而不是新建线程)。

在这里,我们找到了最终的实现,通过取 Worker 的 firstTask 或者 getTask方法从 workQueue 中取出了新任务,并直接调用 Runnable 的 run 方法来执行任务,也就是如之前所说的,每个线程都始终在一个大循环中,反复获取任务,然后执行任务,从而实现了线程的复用。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

k8s-kubectl命令详解、Pod创建过程、Pod的生命周期、定制Pod、资源对象文件

集群管理 一、如何管理集群 kubectl是用于管理Kubernetes集群的命令行工具 二、语法格式&#xff1a; kubectl [command] [TYPE] [NAME] [flags] command&#xff1a;子命令&#xff0c;如create&#xff0c;get&#xff0c;describe&#xff0c;delete type&#xff1a;…

拼多多2023年实现营收2476亿 助力品质好物与消费升级双向奔赴

拼多多集团近日发布了截至去年12月31日的财务业绩报告&#xff0c;拼多多在2023年第四季度实现了889亿元的营收&#xff0c;同比增长了惊人的123%。而在全年范围内&#xff0c;拼多多的营收更是高达2476亿元&#xff0c;同比增长了90%。 去年是拼多多全面拥抱高质量发展的元年…

流水灯的实现

#include<reg51.h> //点亮一个LED灯&#xff0c;并使其闪烁 sbit LED0P2^0; void delay(int n) {int i;for(i0;i<n;i); } void main() {while(1){LED00; //亮delay(6000);LED01;delay(6000);} } #include<reg51.h> //实现流水灯 void delay(int n) {int i;fo…

Jenkins安装 Linux 更换镜像 安装插件

Jenkins安装 Linux 更换镜像 安装插件 前言 下面叙述了三种jenkins安装的方式,jenkins安装之前必须有java环境因为他是java写的… yum安装只能安装最新版本的jenkins,但是jenkins是java写的所以他强依赖java版本,当你的服务器的java版本与jenkins版本冲突时还需要给jenkins重…

学浪视频怎么保存到本地

现在随着知识付费的兴起&#xff0c;抖音也下场做知识付费&#xff0c;做了一个学浪平台&#xff0c;可是它却不提供下载按钮&#xff0c;但我们又需要把学浪视频保存到本地 这里就教大家如何将学浪视频保存到本地 由于有些小白不懂技术&#xff0c;他只想下载下来视频&#…

UnityShader(十九) AlphaBlend

上代码&#xff1a; Shader "Shader入门/透明度效果/AlphaBlendShader" {Properties{_MainTex ("Texture", 2D) "white" {}_AlphaScale("AlphaScale",Range(0,1))1.0}SubShader{Tags { "RenderType""Transparent&quo…

删除列表中指定索引对应的元素删除字典中指定键对应的元素operator.delitem(d, p)

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 删除列表中指定索引对应的元素 删除字典中指定键对应的元素 operator.delitem(d, p) [太阳]选择题 关于operator.delitem()的使用方法和功能正确的是 import operator list [11, 22, 33, 44,…

实体框架EF(Entity Framework)简介

实体框架EF&#xff08;Entity Framework&#xff09;简介 文章目录 实体框架EF&#xff08;Entity Framework&#xff09;简介一、概述二、O/R Mapping是什么采用O/R Mapping带来哪些好处 三、Entity Framework架构3.1 下图展示了Entity Framework的整体架构3.2 Entity Framew…

STL中 function 源码解析

1. function 本文基于 GCC 9.4 function 的作用就是将各种仿函数的调用统一起来&#xff1b; 1.1 类中非静态成员函数指针为什么是16字节 auto cptr &A::myfunc; 类中非静态成员函数 &#xff0c;其类型为 void (A::*)(int) auto rptr print_num; 普通函数对应汇…

MyBatis-Plus 实用工具:SqlHelper 让你的数据库操作更得心应手

一、SqlHelper是什么&#xff1f; SqlHelper 是MyBatis-Plus的一款SQL 辅助工具类&#xff0c;提供了一些常用的方法&#xff0c;简便我们的操作&#xff0c;提高开发效率。文档 二、示例代码 public class SqlHelperDemo {public static void main(String[] args) {// 示例…

【spring】@Lazy注解学习

Lazy介绍 Lazy 注解是一个配置注解&#xff0c;用于指示 Spring 容器在创建 bean 时采用延迟初始化的策略。这意味着&#xff0c;除非 bean 被实际使用&#xff0c;否则不会被创建和初始化。 在 Spring 框架中&#xff0c;默认情况下&#xff0c;所有的单例 bean 在容器启动时…

运用YOLOv5实时监测并预警行人社交距离违规情况

YOLO&#xff08;You Only Look Once&#xff09;作为一种先进的实时物体检测算法&#xff0c;在全球范围内因其高效的实时性能和较高的检测精度受到广泛关注。近年来&#xff0c;随着新冠疫情对社交距离管控的重要性日益凸显&#xff0c;研究人员开始将YOLO算法应用于社交距离…

关于Count,FPKM,TPM,RPKM等表达量的计算及转换 | 干货

原文链接:关于Count,FPKM,TPM,RPKM等表达量的计算及转换 | 干货 写在前面 今天使用count值转化TPM,或是使用FPKM转换成TPM。这样的教程,我们在前面已经出国一起相对比较详细的教程了,一文了解Count、FPKM、RPKM、TPM | 相互间的转化,在这个教程中,我们也归纳了各个数…

【力扣hot100】128.最长连续序列

给定一个未排序的整数数组 nums &#xff0c;找出数字连续的最长序列&#xff08;不要求序列元素在原数组中连续&#xff09;的长度。 请你设计并实现时间复杂度为 O(n) 的算法解决此问题。 示例 1&#xff1a; 输入&#xff1a;nums [100,4,200,1,3,2] 输出&#xff1a;4 解…

DMA的定义和作用

在计算机系统中&#xff0c;DMA&#xff08;Direct Memory Access&#xff0c;直接内存访问&#xff09;是一种用于提高数据传输效率的重要技术。本文将介绍DMA的定义、原理和作用&#xff0c;以及它在计算机系统中的重要性。 以下是我整理的关于嵌入式开发的一些入门级资料&a…

app开发中HBuilderX运行模拟器 配置模拟器手册

1.首先打开HBuilder 然后点击,左上角运行 2.点击运行到手机或模拟器内的ADB路径设置(A) 3. adb配置你模拟器的 adb.exe路径端口号配置你模拟器的端口号 我这里使用的逍遥模拟器所以 | 21503 端口 | 手机模拟器名称21503 端口逍遥模拟器21503夜神模拟器62001网易mumu模拟器7…

点餐小程序php毕设项目

主要技术框架&#xff1a; 主要功能模块&#xff1a; 商品管理 订单管理 用户管理 优惠券管理 商品分类管理 评论管理 轮播图管理 截图 获取源码 https://blog.lusz.top/article?article_id-2

Talk|Mila研究所蒙特利尔大学刘圳:三维表征和三维网格的重建与生成

本期为TechBeat人工智能社区第580期线上Talk。 北京时间3月21日(周四)20:00&#xff0c;Mila研究所&蒙特利尔大学博士生—刘圳的Talk已经准时在TechBeat人工智能社区开播&#xff01; 他与大家分享的主题是: “三维表征和三维网格的重建与生成”&#xff0c;向大家系统地介…

【软考】生成树

目录 1. 概念2. 图解3. 例题3.1 例题1 1. 概念 1.对于有n个顶点的连通图&#xff0c;至少有n-1条边&#xff0c;而生成树中恰好有n-1条边2.连通图的生成树是该图的极小连通子图3.若在图的生成树中任意加一条边&#xff0c;则必然形成回路4.图的生成树不是唯一的5.从不同的顶点…

GPU云服务器的优势和应用

GPU即图形处理器&#xff0c;是一种高性能计算加速器&#xff0c;主要用于处理复杂的图像、视频等。GPU云服务器&#xff0c;指的是在云计算环境下&#xff0c;通过云平台提供GPU计算能力的虚拟服务器。随着科技的迅猛发展&#xff0c;科技领域对其的讨论和应用也日益热烈、广泛…