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

news2024/12/22 6:02:31

问题:

线程池的核心线程数、最大线程数该如何设置?
线程池执行任务的具体流程是怎样的?
线程池的五种状态是如何流转的?
线程池中的线程是如何关闭的?
线程池为什么一定得是阻塞队列?
线程发生异常,会被移出线程池吗?
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); 3
 execute(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 checks
        if (parent == null) {
            return super.offer(o);
        }

        //we are maxed out on threads, simply queue the object
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) {
            return super.offer(o);
        }

        //we have idle threads, just add it to the queue
        if (parent.getSubmittedCount() <= (parent.getPoolSize())) {
            return super.offer(o);
        }

        //if we have less threads than maximum force creation of a new thread
        if (parent.getPoolSize() < parent.getMaximumPoolSize()) {
            return false;
        }

        //if we reached here, we need to add it to the queue
        return 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
以上只是理论,实际工作中情况会更复杂,比如一个应用中,可能有多个线程池,除开线程池中的线 程可能还有很多其他线程,或者除开这个应用还是一些其他应用也在运行,所以实际工作中如果要确 定线程数,最好是压测。
比如写一个:
@RestController
 public 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.coloradmin.cn/o/1119813.html

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

相关文章

蓝桥杯 (饮料换购,C++)

思路&#xff1a; 1、先加上初始的饮料数n。 2、再加上n可以兑换的饮料数n/3&#xff0c;求多余的瓶盖n%3。循环直至瓶盖数无法兑换新的一瓶饮料。 #include<iostream> using namespace std; int main() {int n,a0,sum0;cin >> n;sum n;while (n){n n a;//加上上…

【软考】11.5 测试原则/阶段/测试用例设计/调试

《测试原则和方法》 测试原则 测试&#xff1a;为了发现错误而执行程序的过程成功的测试&#xff1a;发现了至今尚未发现的错误的测试 测试方法 静态测试&#xff08;有效发现30%-70%的错误&#xff09; a. &#xff08;文档&#xff09;检查单 b. &#xff08;代码&#xff…

学习SpringMVC,建立连接,请求,响应 SpringBoot初学,如何前后端交互(后端版)?最简单的能通过网址访问的后端服务器代码举例

要想通过SpringBoot写一个简单的处理请求的服务器&#xff08;方法&#xff09;&#xff0c;需要有以下步骤 建立连接请求响应 来复习的话直接在文章末尾看源码就行 1、创建SpringBoot项目 https://blog.csdn.net/dream_ready/article/details/133948253 2、编写Controller建…

WebSocket的入门秘籍?

一、是什么 WebSocket&#xff0c;是一种网络传输协议&#xff0c;位于OSI模型的应用层。可在单个TCP连接上进行全双工通信&#xff0c;能更好的节省服务器资源和带宽并达到实时通迅 客户端和服务器只需要完成一次握手&#xff0c;两者之间就可以创建持久性的连接&#xff0c…

postman打开后,以前的接口记录不在,问题解决

要不这些文件保存在C:\Users\{用户名}\AppData\Roaming\Postman 比如&#xff0c;你目前使用的window登录用户是abc&#xff0c;那么地址便是C:\Users\abc\AppData\Roaming\Postman 打开后&#xff0c;这个目录下会有一些命名为backup-yyyy-MM-ddThh-mm-ss.SSSZ.json类似的文…

[python 刷题] 287 Find the Duplicate Number

[python 刷题] 287 Find the Duplicate Number 题目&#xff1a; Given an array of integers nums containing n 1 integers where each integer is in the range [1, n] inclusive. There is only one repeated number in nums, return this repeated number. You must sol…

zookeeper的介绍和用docker搭建zookeeper集群,以及Go语言使用zookeeper

typora-copy-images-to: imgs Zookeeper的使用 1、Zookeeper简介 Apache ZooKeeper 是 Apache 软件基金会的一个软件项目&#xff0c;为大型分布式系统提供开源分布式配置服务、同步服务和命名注册。ZooKeeper原本是Hadoop的一个子项目&#xff0c;但现在它本身已经是一个顶级…

新年学新语言Go之三

一、前言 这一篇简单介绍一下Go中的数组、切片、map和指针。 二、数组 Go语言的数组和Java差不多都是定长的&#xff0c;用于存储有相同类型的元素&#xff0c;数组在内存中是连续分配的&#xff0c;索引数组中任意数据速度都非常快。 注&#xff1a;Go声明变量和其它强类型…

C++前缀和算法的应用:得到连续 K 个 1 的最少相邻交换次数 原理源码测试用例

本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 滑动窗口 题目 给你一个整数数组 nums 和一个整数 k 。 nums 仅包含 0 和 1 。每一次移动&#xff0c;你可以选择 相邻 两个数字并将它们交换。 请你返回使 nums 中包…

FL Studio中文最新21破解版本水果软件下载

那么&#xff0c;大家知道编曲是什么吗&#xff1f;编曲和作曲又有什么区别呢&#xff1f; 一首歌的制作过程通常是由作词或作曲开始的&#xff0c;作曲就是运用基本乐理、和声学、复调、配器法、曲式结构的技术理论体系来表达创作者音乐思想的方法。说白了其实就是制作一首歌…

学信息系统项目管理师第4版系列34_10大管理49过程ITTO

整合管理 组 过程 输入 工具和技术 输出 启动 制定项目章程 立项管理文件协议事业环境因素组织过程资产 专家判断数据收集人际关系与团队技能会议 项目章程假设日志 计划 2.制定项目管理计划 项目章程其他知识领域规划过程的输出事业环境因素组织过程资产 专家…

【软考】9.5 排序算法原理

《直接插入排序》 针对少量数据的排序情况多次比较&#xff0c;一次插入 默认第一个元素为有序队列&#xff0c;依次与前面的元素进行比较&#xff0c;直到找到第一个小于他的值&#xff0c;才插入 《希尔排序》 缩小增量排序&#xff1b;针对大数据的排序情况分组&#xff0…

STM32cubemx对FreeRTOS的适配(工程模板配置)

文章目录 前言一、工程的创建二、什么是CMSIS三、STM32cubemx生成的FreeRTOS工程分析总结 前言 本篇文章将带大家使用STM32cubemx对FreeRTOS进行工程模板的配置。 一、工程的创建 1.开始工程的创建&#xff1a; 2.芯片型号选择&#xff1a; 3.修改时钟为TIM8&#xff1a; …

00-开源离线同步工具DataX3.0重磅详解!

1 概览 DataX 是一个异构数据源离线同步工具&#xff0c;致力于实现包括关系型数据库(MySQL、Oracle等)、HDFS、Hive、ODPS、HBase、FTP等各种异构数据源之间稳定高效的数据同步功能。 1.1 设计理念 为了解决异构数据源同步问题&#xff0c;DataX将复杂的网状的同步链路变成了…

Tomcat部署项目的两种方式

第一种: 将项目放到tomcat的webapps目录下,war包会自动解压 里面有个页面 为什么会默认访问asd.html 可以配置 tomcat--->conf---->web.xml 第二种方式 在Tomcat/conf/Catalina/localhost/目录下随便建个xxx.xml文件 注意字符编码 utf-8 注意aaa就是上下文地址 …

【计算机网络笔记】OSI参考模型中端-端层(传输层、会话层、表示层、应用层)功能介绍

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

C++笔记之遍历vector的所有方式

C笔记之遍历vector的所有方式 —— 2023年4月15日 上海 code review 文章目录 C笔记之遍历vector的所有方式1.普通for循环2.迭代器版3.const迭代器4.C11引入的范围for循环5.使用auto关键字和迭代器6.使用std::for_each算法7.使用std::for_each和lambda表达式8.普通版vector::at…

API接口随心搭,自由定制你的数据流

API接口是现代软件开发中非常重要的一部分。API接口可以帮助不同的系统和应用程序之间进行数据交换和通信。随着计算机网络技术的不断发展&#xff0c;API接口的适用范围也越来越广泛。如今&#xff0c;在互联网上许多网站和应用程序都提供了各种各样的API接口&#xff0c;供开…

html5语义化标签

目录 前言 什么是语义化标签 常见的语义化标签 语义化的好处 前言 HTML5 的设计目的是为了在移动设备上支持多媒体。之前网页如果想嵌入视频音频&#xff0c;需要用到 flash &#xff0c;但是苹果设备是不支持 flash 的&#xff0c;所以为了改变这一现状&#xff0c;html5 …

自然语言处理---Tr ansformer机制详解之Transformer结构

1 Encoder模块 1.1 Encoder模块的结构和作用 经典的Transformer结构中的Encoder模块包含6个Encoder Block.每个Encoder Block包含一个多头自注意力层&#xff0c;和一个前馈全连接层. 1.2 Encoder Block 在Transformer架构中&#xff0c;6个一模一样的Encoder …