线程池:优化多线程管理的利器

引言

同步和异步想必各位都有了解,同步简单来说就是一件事做完再去做下一件;异步则是不用等一件事做完,就可以去做另一件事,当一件事完成后可以收到对应的通知;异步一般应用于一些耗时较长的操作,比如大型文件的上传、第三方接口的请求等,可以大大提升系统效率,优化用户体验;但是对于执行这些异步任务的线程管理相对复杂,什么时候异步线程执行结束?如何控制线程的创建数目?线程繁忙时最新的任务该如何处理(舍弃 or 暂存)?这些问题就大大增加了异步编程的复杂性,但是线程池可以很方便的解决这些问题。

在多线程编程中,线程的创建和销毁是一项开销较大的任务。为了更有效地利用系统资源、提高程序的性能,线程池应运而生。线程池是一种管理和复用线程的机制,通过预先创建一定数量的线程,它们可以被重复利用来处理多个任务,避免了线程频繁创建和销毁的开销,提升了系统的性能和响应速度。

线程池有以下优点:

  • 提高系统性能:线程池在高并发的场景中能够有效地提高系统的性能。通过限制并发线程的数量,避免系统因过多线程而产生过多的上下文切换,提升整体系统的处理能力。

  • 控制资源并发度:在资源有限的环境下,线程池可以用来控制并发度,确保系统资源不被过度占用。例如,在进行文件下载时,可以通过线程池限制同时下载的文件数量,避免过多的网络连接占用带宽。

  • 提高响应速度:线程池能够在任务到达时立即执行,而不需要等待新线程的创建,从而提高了系统对外响应的速度。这在需要快速响应用户请求的网络服务中尤为重要。

  • 避免线程泄漏:通过线程池,线程的生命周期由线程池进行管理,可以避免线程泄漏问题。当任务执行完毕后,线程池会将线程放回线程池,而不是销毁它,从而减少了资源浪费。

  • 任务排队和管理:线程池提供了对任务的排队和管理机制。可以按照优先级、先进先出等规则对任务进行排队,确保高优先级的任务先得到执行。

  • 避免系统崩溃:在某些情况下,如果系统同时创建大量线程,可能导致系统资源耗尽,甚至引发系统崩溃。线程池通过控制线程的数量,可以防止系统因为线程过多而崩溃的情况发生。

总的来说线程池主要帮我们解决线程的管理和任务的存取功能,实现更方便的管理线程和协调任务的执行;

线程池的实现

java提供的juc包中已经提供了很方便的方法区创建线程池的类:ThreadPoolExecutor,主要介绍一下该方法的参数:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

ThreadPoolExecutor 是 Java 中用于管理线程池的类,用于执行异步任务。在创建 ThreadPoolExecutor 实例时,可以传递一系列参数来配置线程池的行为

  • corePoolSize: 表示线程池的基本大小,即线程池中保持活动状态的最小线程数。(核心线程数,系统能同时工作的线程数;理解为一个公司的核心成员,不会被开除)

  • maximumPoolSize: 表示线程池的最大大小,即线程池中允许的最大线程数。当工作队列满了,且活动线程数小于最大线程数时,会创建新的线程来处理任务。(最大线程数,理解为一个公司的最多所有成员数目,核心成员外包成员(会被开除),当项目太多的时候(工作队列满),就开始招聘外包成员,但招的外包成员加上核心成员不会超过maximumPoolSize)

  • keepAliveTime: 表示线程空闲时的存活时间。当线程池中的线程数量超过 corePoolSize,且某个线程(非核心线程)空闲时间超过 keepAliveTime,该线程将被终止,直到线程池中的线程数不超过 corePoolSize。(当项目完成后(工作队列空),且等待keepAliveTime这么长时间后,开除外包员工(销毁非核心线程))

  • unit:keepAliveTime 配合使用,表示 keepAliveTime 的时间单位,通常是秒、毫秒等。

  • workQueue: 用于保存等待执行的任务的阻塞队列(工作队列)。可以选择不同的队列实现,如 LinkedBlockingQueueArrayBlockingQueue 等,用于控制任务的排队策略。(工作队列需要设置一个合适的长度,太长了也会占用系统资源)

  • threadFactory: 用于创建新线程的工厂。提供了一种自定义线程创建的机制,可以用来设置线程的名称、优先级等。

  • handler: 表示当线程池中的工作线程达到最大线程数并且阻塞队列已满时的饱和策略。可以选择使用预定义的策略,查看源码可以看到提供有四中策略:
    在这里插入图片描述
    其中默认策略为AbortPolicy:
    image-20240125222517330

    AbortPolicy (默认策略): 默认饱和策略。当任务无法提交给线程池执行时,会抛出 RejectedExecutionException 异常,通知调用者任务无法被接受。

    CallerRunsPolicy: 将任务返回给调用线程来执行(一般是主线程)。这种策略不会抛出异常,而是尝试在调用线程中执行该任务。

    DiscardPolicy: 会默默地放弃无法处理的任务,不提供任何通知或记录。

    DiscardOldestPolicy: 在队列满的情况下,尝试将最早进入队列的任务移除,为新任务腾出空间,然后将新任务添加到队列中,如果失败则按该策略不断重试,不会抛出异常。

    当然也可以自定义策略实现。
    需要根据实际情况合理设置核心线程数、最大线程数和阻塞队列,可以在不同的场景下平衡线程池的性能和资源消耗。


下面画图理解一下:

假设核心线程有2个(corePoolSize),线程池最多能有4个线程(maximumPoolSize),任务队列最多能容纳4个任务(workQueue)。

(2个核心员工,最多再招2两个外包)

初始状态

image-20240125204125140

来了两个任务

当线程池有空闲线程时,有了最新任务不进任务队列,直接分配给空闲线程处理;

image-20240125204920882

任务1和任务2未处理完(无空闲线程),此时又来了四个任务

此时并不会创建非核心线程处理最新任务,而是先把最新任务加入任务队列存储,此时任务队列已满

image-20240125212301991

任务1和任务2未处理完(无空闲线程),并且任务队列已有四个任务已满,此时又来了两个任务

此时线程池就会新增两个非核心线程(线程池最多容纳四个线程),用来处理新来的两个任务,但不是优先处理队列中的任务;

(若此时只来了一个任务,则只创建一个非核心线程处理该任务)

image-20240125212354612

若线程池已满,且无空闲线程,并且任务队列已满,此时若又来了一个任务

这时就会触发饱和策略handler进行处理,比如舍弃该任务,或者主线程介入处理等,默认情况时拒绝处理,会抛出RejectedExecutionException异常;

image-20240125212457519

当线程3和线程4空闲后(此时所有任务已处理完),当空闲时间达到最大空闲时间时,线程3和线程4被释放,线程池只剩下线程1和线程2两个核心线程;


这就是线程池的处理大致流程,一定要记住那几个线程池核心参数,结合图中内容理解。

在实际开发中难点主要是如何确定线程池参数,一定要涉及到多方面的考虑,包括应用的性质、系统资源、任务的性质等。这里提供一些判断的思路;

corePoolSize: 根据应用的负载情况和任务的性质设置。如果任务是计算密集型,并且负载比较高,可以增加 corePoolSize。对于 I/O 密集型任务,可以适度减小 corePoolSize

maximumPoolSize: 根据系统资源和任务性质进行设置。如果任务是 I/O 密集型,并且可能发生阻塞,可以适度增加 maximumPoolSize。但是,不宜过度设置,以免占用过多的系统资源。

keepAliveTime 和 unit: 适度设置,避免线程过早终止或过长存活。如果任务的执行时间相对较短,可以考虑设置较短的 keepAliveTime,一般设置为秒级或者分钟级。

workQueue: 选择适合应用场景的队列类型。对于有界队列(如 ArrayBlockingQueue),可以帮助控制线程数,防止无限制的线程增长,但也可能导致任务被拒绝。无界队列(如 LinkedBlockingQueue)可以避免任务被拒绝,但可能导致线程数无限增长。

threadFactory: 根据需要进行设置,可以用于为线程指定有意义的名称、设置优先级等。

handler: 根据应用的容错需求选择合适的饱和策略。例如,CallerRunsPolicy 可以将任务退回给调用线程,避免任务丢失,但可能导致调用线程也过载。

补充一下IO密集型和计算密集型:

一般情况下,任务可以分为IO密集型和计算密集型(CPU密集型);

  • 对于IO密集型任务,主要由于涉及到大量的读写操作,如文件操作、网络通信、数据库访问等操作,在这种任务类型中,大部分时间线程都在等待 I/O 操作完成,而不是在执行计算操作,即对CPU的利用率不高。由于线程大部分时间都在等待 I/O 操作,可以使用较大的核心线程数和最大线程数,以确保有足够的线程可以处理等待的 I/O 操作,提高任务响应性,但是还是要根据设备的I/O性能来设定。
  • 对于计算密集型任务,其中涉及大量的计算操作,例如数学运算、图形处理等。在这种任务类型中,线程大部分时间都在执行计算操作,而不是等待 I/O 操作。所以可以使用较小的核心线程数和最大线程数,避免占用过多的系统资源和减少线程间的冲突,因为增加线程数可能导致竞争和上下文切换。一般将核心线程数设置为 CPU 的核数加一。这个"加一”可以理解为预留一个额外的线程,或者说一个备用线程,用来处理其他任务。这样做可以充分利用每个CPU 核心,减少线程间的频繁切换,降低开销;在这种情况下,对 maximumPoolSize 的设定没有严格的规则,一般可以设为核心线程数的两倍或三倍。

补充思路:反向压力可以动态控制线程数,简单来说系统压力小时可以多增加几个线程,压力大时减少几个线程,或者可以系统压力大时使用异步,压力小时使用同步,这一块属于大数据内容,后续学习了再补充;

代码实现

下面用一个代码案例模拟一下,以便更直观的理解和应用;

还是用上面的案例:假设核心线程有2个(corePoolSize),线程池最多能有4个线程(maximumPoolSize),任务队列最多能容纳4个任务(workQueue),非核心线程释放时间为100s

先自定义一个线程池的Bean

@Configuration
public class ThreadPoolExecutorConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor() {
        ThreadFactory threadFactory = new ThreadFactory() { // 自定义线程工厂的
            private int count = 1;
            @Override
            public Thread newThread( Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("线程" + count); // 设置线程名称
                count++;
                return thread;
            }
        };

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, 
                                                                       new ArrayBlockingQueue<>(4), threadFactory);
        return threadPoolExecutor;
    }
}

此时没有设置饱和策略,使用的是默认AbortPolicy策略,即抛出异常;

写一个测试Controller

@RestController
@RequestMapping("/queue")
@Slf4j
public class QueueController {

    @Resource
    private ThreadPoolExecutor threadPoolExecutor;

    // 新增任务
    @GetMapping("/add")
    public void add(String name) {
        CompletableFuture.runAsync(() -> {
            log.info("任务执行中:" + name + ",执行线程:" + Thread.currentThread().getName());
            try {
                Thread.sleep(600000); // sleep模拟执行一个耗时的任务
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, threadPoolExecutor);
    }

    // 获取当前线程池状态
    @GetMapping("/get")
    public String get() {
        Map<String, Object> map = new HashMap<>();
        int size = threadPoolExecutor.getQueue().size();
        map.put("queue size", size);
        long taskCount = threadPoolExecutor.getTaskCount();
        map.put("task count", taskCount);
        long completedTaskCount = threadPoolExecutor.getCompletedTaskCount();
        map.put("complete task", completedTaskCount);
        int activeCount = threadPoolExecutor.getActiveCount();
        map.put("active thread", activeCount);
        return JSONUtil.toJsonStr(map);
    }
}

调用add接口,先新增两个任务(1、2),则两个核心线程(线程1和线程2)被占用:

在这里插入图片描述
在这里插入图片描述

控制台输出:
image-20240125224715891
再增加四个任务(3、4、5、6),此时任务队列已满:
在这里插入图片描述
断点调试get接口可以看到任务队列中等待的任务:
image-20240125225006584

再增加两个任务(7、8),则非核心线程会创建(线程3和线程4)处理这两个任务,此时共4个线程,已达到最大线程数:

在这里插入图片描述
image-20240125225201924
此时如果再增加任务(9),则会触发默认AbortPolicy饱和策略,抛出异常:

在这里插入图片描述

直到任务队列有空位时才可以继续增加任务。

线程1、2、3、4执行完各自的任务后依次执行队列中的任务:

在这里插入图片描述
在这里插入图片描述

最后非核心线程执行完成后100s后空闲自动释放:
在这里插入图片描述

这就是大致的线程池操作了,线程池核心参数和整个任务分配和线程管理的流程是关键。

总结

线程池是优化并发执行的关键工具,通过合理配置参数、选择适当的任务队列和饱和策略,可以充分发挥其优势。在实际应用中,根据任务类型、负载和性能需求进行调整,动态监控线程池的状态,将使得线程池更好地适应不同的应用场景,提高系统的并发性能。

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

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

相关文章

量子计算:数据安全难题

当今数字技术面临的最大挑战之一是安全系统和数据。为此&#xff0c;人们设计了复杂的算法来加密数据并通过称为对称加密的框架来保护数据。虽然这已被证明是成功的&#xff0c;但量子计算的进步&#xff08;利用量子力学比传统计算机更快地解决复杂问题&#xff09;可能会彻底…

Flink的单元测试介绍及示例

本文详细的介绍了Flink的单元测试&#xff0c;分为有状态、无状态以及作业的测试&#xff0c;特别是针对无状态的单元测试给出了常见的使用示例。 本文除了maven依赖外&#xff0c;没有其他依赖。 一、Flink测试概述 Apache Flink 同样提供了在测试金字塔的多个级别上测试应用程…

离谱,华为食堂也要搞末位淘汰

华为饭堂 末位淘汰 今天逛职场 App&#xff0c;无意间翻到一篇帖子&#xff1a; 点开图片之前&#xff0c;我还以为只是普通的争霸赛被网友解读为末位淘汰。 点开图片后我却发现 ... 可以看出&#xff0c;是深圳华为的行政部做的海报&#xff0c;里面清晰写到&#xff1a;员工的…

QT-地形3D

QT-地形3D 一、 演示效果二、关键程序三、下载链接 一、 演示效果 二、关键程序 #include "ShaderProgram.h"namespace t3d::core {void ShaderProgram::init() {initializeOpenGLFunctions();loadShaders(); }void ShaderProgram::addShader(const QString &fil…

如何使用Docker搭建YesPlayMusic网易云音乐播放器并发布至公网访问

文章目录 1. 安装Docker2. 本地安装部署YesPlayMusic3. 安装cpolar内网穿透4. 固定YesPlayMusic公网地址 本篇文章讲解如何使用Docker搭建YesPlayMusic网易云音乐播放器&#xff0c;并且结合cpolar内网穿透实现公网访问音乐播放器。 YesPlayMusic是一款优秀的个人音乐播放器&am…

JS逆向进阶篇【去哪儿旅行登录】【中篇-滑动轨迹破解补浏览器环境破参数】

目录&#xff1a; 每篇前言&#xff1a;0、整体分析1、逆向轨迹snapshot&#xff08;1&#xff09;分析&#xff1a;&#xff08;2&#xff09;Python轨迹生成&#xff1a;&#xff08;3&#xff09;AES加密&#xff1a;&#xff08;4&#xff09;轨迹加密&#xff1a;&#xf…

springcloud:1.Eureka详细讲解

Eureka 是 Netflix 开源的一个服务注册和发现工具,被广泛应用于微服务架构中。作为微服务架构中的核心组件之一,Eureka 提供了服务注册、发现和失效剔除等功能,帮助构建弹性、高可用的分布式系统。在现代软件开发领域,使用 Eureka 可以有效地管理和监控服务实例,实现服务之…

Qt Creator在#include第三方库不带.h后缀的文件时,没有智能提示和自动补全

1、问题截图 OSG文件目录下有很多头文件&#xff08;均不带.h后缀&#xff09;&#xff0c;Qt Creator可以识别到OSG目录&#xff0c;但是OSG目录下的所有头文件识别不到 2、原因 找到原因是因为Qt Creator开启了ClanCodeModel插件导致的 3、解决方法 1、在Qt Creator中…

GenAI的“关键一跃”:推理与知识

当前的人工智能领域正通过生成式人工智能&#xff08;GenAI&#xff09;经历一场重大转变。这一转变不仅代表了技术上的飞跃&#xff0c;更标志着人工智能领域的范式转变&#xff0c;引发了有关GenAI的独特特性及其深远影响的关键问题讨论。 植根于计算革命的丰富历史&#xff…

OpenCV人脸检测案例实战

人脸检测是一种计算机视觉技术&#xff0c;旨在识别图像或视频中的人脸。这项技术的基本内容包括使用特定的算法和模型来定位和识别人脸&#xff0c;通常涉及在图像中寻找面部特征&#xff0c;如眼睛、鼻子、嘴巴等&#xff0c;以便准确地确定人脸的位置和边界。人脸检测技术的…

LeetCode JS专栏刷题笔记(一)

一、前言 LeetCode 在前不久出了一个 JavaScript 专栏&#xff0c;这个专栏一个目的是为了非前端工程师学习 JS&#xff0c;另一个是为了前端工程师提升 JS 能力。 因此在这个专栏中&#xff0c;基本不涉及什么具体算法问题&#xff0c;都是一些 JS 的入门语法与常见的 JS 面…

安卓游戏开发之图形渲染技术优劣分析

一、引言 随着移动设备的普及和性能的提升&#xff0c;安卓游戏开发已经成为一个热门领域。在安卓游戏开发中&#xff0c;图形渲染技术是关键的一环。本文将对安卓游戏开发中常用的图形渲染技术进行分析&#xff0c;比较它们的优劣&#xff0c;并探讨它们在不同应用场景下的适用…

从零开始:开发多商户商城APP的技术指南

当下&#xff0c;电子商务正在飞速发展&#xff0c;多商户商城APP的需求也与日俱增。本篇文章&#xff0c;小编将为大家深度详解如何开发多商户商城APP。 1.确定功能需求 在着手开发多商户商城APP之前&#xff0c;首先需要明确功能需求。这包括但不限于&#xff1a; -用户注…

如何在CentOS安装SQL Server数据库并实现无公网ip环境远程连接

文章目录 前言1. 安装sql server2. 局域网测试连接3. 安装cpolar内网穿透4. 将sqlserver映射到公网5. 公网远程连接6.固定连接公网地址7.使用固定公网地址连接 前言 简单几步实现在Linux centos环境下安装部署sql server数据库&#xff0c;并结合cpolar内网穿透工具&#xff0…

MongoDB文档插入

文章目录 MongoDB文档插入对比增删改查文档插入 MongoDB写安全机制非确认式写入 MongoDB文档查询参数说明查询操作符比较查询操作符逻辑查询操作符元素查询操作符数组查询操作符 模糊查询区别:$regex操作符中的option选项 MongoDB游标介绍游标函数手动迭代游标示例游标介绍 Mon…

揭秘智能商品计划管理系统:为何服装企业老板争相引入?

在如今日新月异的商业环境中&#xff0c;服装企业老板们纷纷将目光转向了一种名为“智能商品计划管理系统”的创新工具。这种系统不仅具有高度的自动化和智能化特性&#xff0c;还能显著提升企业的运营效率、减少库存积压&#xff0c;并帮助企业在激烈的市场竞争中占据优势地位…

xilinx除法器的使用

平台&#xff1a;Vivado2018.3. 芯片&#xff1a;xcku115-flva1517-2-i (active) 最近学习使用了xilinx除法器&#xff0c;在使用过程中出现了很多次除法器的结果和我预计的结果不一致&#xff0c;特此记录学习一下。 参考文件&#xff1a;pg151.下载地址 pg151-div-gen.pdf …

简单了解一下加密算法

1.1单向散列算法 单向散列函数算法也称 Hash(哈希)算法&#xff0c;是一种将任意长度的消息压缩到某一固定长度(消 息摘要)的函数(该过程不可逆)。Hash 函数可用于数字签名、消息的完整性检测、消息起源的认 证检测等。常见的散列算法有MD5 、SHA 、RIPE-MD 、HAVAL 、N-Hash等…

【Pygame手册03/20】用 pygame 模块draw绘制形状

目录 一、说明二、画图函数2.1 接口draw下的函数2.2 pygame.draw.rect()2.3 pygame.draw.polygon()2.4 pygame.draw.circle()2.5 pygame.draw.ellipse()2.6 pygame.draw.arc()2.7 pygame.draw.line ()2.8 pygame.draw.lines()2.9 pygame.draw.aaline()2.10 pygame.draw.aaline…

【EI会议征稿通知】2024年通信安全与信息处理国际学术会议(CSIP 2024)

2024年通信安全与信息处理国际学术会议&#xff08;CSIP 2024) 2024 International Conference on Communication Security and Information Processing 随着全球信息化的深入发展&#xff0c;通信安全与信息处理已成为当今社会关注的热点问题。为了加强国际间的学术交流与合…