源码进阶之线程池

2023-10-27

写在前面
  上次学习了多线程,了解了线程的概念和作用,学习了线程的创建方式、工作模式和一些重要的方法。当我们使用线程中,创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,就会很大程度上影响处理效率,那么此时我们就引入了线程池的概念,即为避免线程频繁的创建和销毁带来的性能消耗,而建立的一种池化技术,它是把已创建的线程放入“池”中,当有任务来临时就可以重用已有的线程,无需等待创建的过程,这样就可以有效提高程序的响应速度。下面就来学习下线程池的创建和使用方法。
  认识线程池之源 ThreadPoolExecutor
  线程池的话一定离不开 ThreadPoolExecutor ,在阿里巴巴的《Java 开发手册》中是这样规定线程池的:
  线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的读者更加明确线程池的运行规则,规避资源耗尽的风险。
  说明:Executors 返回的线程池对象的弊端如下:
  FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
  CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
  其实我们通过看源码不难发现,Executors的一些底层就是ThreadPoolExecutor,所以我们只要学习ThreadPoolExecutor 的相关知识,就能掌握线程池的使用。
  下面我们就来看看ThreadPoolExecutor 的源码
  ThreadPoolExecutor有很多构造方法,我们重点看下涉及核心参数较多的构造方法,源码如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

第 1 个参数:corePoolSize 表示线程池的常驻核心线程数。如果设置为 0,则表示在没有任何任务时,销毁线程池;如果大于 0,即使没有任务时也会保证线程池的线程数量等于此值。但需要注意,此值如果设置的比较小,则会频繁的创建和销毁线程;如果设置的比较大,则会浪费系统资源,所以开发者需要根据自己的实际业务来调整此值。
第 2 个参数:maximumPoolSize 表示线程池在任务最多时,最大可以创建的线程数。官方规定此值必须大于 0,也必须大于等于 corePoolSize,此值只有在任务比较多,且不能存放在任务队列时,才会用到。
第 3 个参数:keepAliveTime 表示线程的存活时间,当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数量销毁的等于 corePoolSize 为止,如果 maximumPoolSize 等于 corePoolSize,那么线程池在空闲的时候也不会销毁任何线程。
第 4 个参数:unit 表示存活时间的单位,它是配合 keepAliveTime 参数共同使用的。
第 5 个参数:workQueue 表示线程池执行的任务队列,当线程池的所有线程都在处理任务时,如果来了新任务就会缓存到此任务队列中排队等待执行。
第 6 个参数:threadFactory 表示线程的创建工厂,此参数一般用的比较少,我们通常在创建线程池时不指定此参数,它会使用默认的线程创建工厂的方法来创建线程,我们也可以自定义一个线程工厂,通过实现 ThreadFactory 接口来完成,这样就可以自定义线程的名称或线程执行的优先级了。
第 7 个参数:RejectedExecutionHandler 表示指定线程池的拒绝策略,当线程池的任务已经在缓存队列 workQueue 中存储满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略,它属于一种限流保护的机制。
  下面来看下线程池的工作流程:
  说起线程池的工作流程,要从它的执行方法execute()方法看起,源码如下:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    // 当前工作的线程数小于核心线程数
    if (workerCountOf(c) < corePoolSize) {
        // 创建新的线程执行此任务
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 检查线程池是否处于运行状态,如果是则把任务添加到队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 再出检查线程池是否处于运行状态,防止在第一次校验通过后线程池关闭
        // 如果是非运行状态,则将刚加入队列的任务移除
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // 如果线程池的线程数为 0 时(当 corePoolSize 设置为 0 时会发生)
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false); // 新建线程执行任务
    }
    // 核心线程都在忙且队列都已爆满,尝试新启动一个线程执行失败
    else if (!addWorker(command, false)) 
        // 执行拒绝策略
        reject(command);
}

其中 addWorker(Runnable firstTask, boolean core) 方法的参数说明如下:
firstTask,线程应首先运行的任务,如果没有则可以设置为 null;
core,判断是否可以创建线程的阀值(最大值),如果等于 true 则表示使用 corePoolSize 作为阀值,false 则表示使用 maximumPoolSize 作为阀值。
  下面我们就来带着一些常见的问题总结学习下线程池
  1.ThreadPoolExecutor 的执行方法有几种?它们有什么区别?
  ThreadPoolExecutor的执行方法主要有两个,即execute() 和 submit()
  execute() 和 submit() 都是用来执行线程池任务的,它们最主要的区别是,submit() 方法可以接收线程池执行的返回值,而 execute() 不能接收返回值。
  上一组测试代码如下:

public static void main(String[] args) throws InterruptedException, ExecutionException {
		// TODO Auto-generated method stub
		ThreadPoolExecutor executor =new ThreadPoolExecutor(2,10,10L,TimeUnit.SECONDS,new LinkedBlockingDeque<>(20));
		executor.execute(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("Hello jbzhang ,welcome use execute()");
			}
		});
			Future<String> future=executor.submit(new Callable<String>() {
				@Override
				public String call() throws Exception {
					// TODO Auto-generated method stub
					System.out.println("Hello jbzhang ,welcome use submit()");
					return "SUCCESS";
				}
			});
		System.out.println(future.get());
	}

  submit() 方法可以配合 Futrue 来接收线程执行的返回值。它们的另一个区别是 execute() 方法属于 Executor 接口的方法,而 submit() 方法则是属于 ExecutorService 接口的方法。
2.什么是线程的拒绝策略?拒绝策略的分类有哪些?我们如何自定义拒绝策略?
  当线程池中的任务队列已经被存满,再有任务添加时会先判断当前线程池中的线程数是否大于等于线程池的最大值,如果是,则会触发线程池的拒绝策略。
  Java 自带的拒绝策略有 4 种:
    AbortPolicy,终止策略,线程池会抛出异常并终止执行,它是默认的拒绝策略;
    CallerRunsPolicy,把任务交给当前线程来执行;
    DiscardPolicy,忽略此任务(最新的任务);
    DiscardOldestPolicy,忽略最早的任务(最先加入队列的任务)。
  自定义拒绝策略只需要新建一个 RejectedExecutionHandler 对象,然后重写它的 rejectedExecution() 方法即可,如下代码所示:

public static void main(String[] args) throws InterruptedException, ExecutionException {
		// TODO Auto-generated method stub
		ThreadPoolExecutor executor =new ThreadPoolExecutor(1,3,10,TimeUnit.SECONDS,new LinkedBlockingDeque<>(2),new RejectedExecutionHandler(){
            //自定义业务处理方法
			@Override
			public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
				// TODO Auto-generated method stub
				System.out.println("Hello jbzhang 执行自定义拒绝策略!");
			}	
		});
		for(int i=0;i<6;i++){
			executor.execute(()->{
				System.out.println(Thread.currentThread().getName());
			});
		}

	}

3.ThreadPoolExecutor 能不能实现扩展?如何实现扩展?
  ThreadPoolExecutor 的扩展主要是通过重写它的 beforeExecute() 和 afterExecute() 方法实现的,我们可以在扩展方法中添加日志或者实现数据统计,比如统计线程的执行时间等。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

源码进阶之线程池 的相关文章

  • 为什么春季测试失败,不起作用@MockBean

    我尝试为一个简单的 spring boot 控制器创建我的第一个测试 但我得到Handler Type null 在浏览器中代码可以工作 但测试失败 我的应用程序使用 spring security 请帮助我解决问题并理解我的错误 谢谢 这
  • 使用现有同级属性值对属性进行 Jackson 多态反序列化

    我有一个现有的Request Response协议使用JSON我无法控制 示例1 响应JSON不需要任何多态反序列化 name simple response params success true 示例2 响应JSON需要对 params
  • 没有绑定 play.db.Database 的实现

    我在使用 hikaricp 时访问数据库时遇到问题 这是我的reference conf play modules enabled play api db DBModule enabled play api db HikariCPModul
  • 如何测试两个 Joda-Time DateTime 对象几乎相等?

    在单元测试中 我经常使用返回DateTime于或关于now 有没有办法说actual日期时间在几秒之内actual约会时间 这听起来是个坏主意 单元测试不应该以任何方式依赖于当前的实际时间 这就是为什么注入一些接口是一个很好的做法 称为Cl
  • 您最好的 Swing 设计模式和技巧是什么? [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • 如何使用 selenium 和 junit 测试多个浏览器(版本)

    我刚刚发现了硒 一个很棒的工具 我计划运行 使用 selenium ide 生成的 junit4 代码 但我需要它与许多浏览器 网络驱动程序一起运行 这个用例有 junit java 模式吗 我的第一个想法是使用 RunWith Param
  • OpenGL ES 2.0 只绘制一次对象

    首先我要说的是 很抱歉今天问了这么多问题 所以 我有一个圈子的课程 我有一个包含 3 个圆形实例的数组列表 每个实例都有不同的 x 坐标来绘制 不管出于什么原因 OpenGL ES 2 0 只绘制其中之一 尽管我调用所有这些来绘制 我检查了
  • 如何解决Spring Data JPA中的N+1问题?

    我使用 Spring Data JPA 作为持久层 并且面临 N 1 问题 我还使用规范 API 因为我发现很难解决 N 1 问题 请帮忙 Entity public class PopulationHealth Id private in
  • Java:输入/使用“try-catch”块的开销?

    这个问题说明了一切 尽管命中率不是很高 我测得速度慢了 1 5 倍到 2 倍 但使用 try catch 的字节码和不使用 try catch 的字节码之间没有区别 那么是什么让它通常变慢呢 PL 请注意 问题不是抛出异常的开销 而是进入
  • 在 IntelliJ IDEA 中编辑并继续?

    使用 IntelliJ IDEA 社区版进行调试时是否可以编辑一些代码 我在选项中找不到这个功能 是的 这就是所谓的 热插拔 您可以在调试过程中编译修改后的代码 并且类文件将被替换 直到您停止调试 确保在调试器设置中启用 HotSwap 选
  • 克隆在幕后是如何工作的?

    克隆不会调用对象构造函数来创建对象的副本 那么clone使用什么算法呢 我正在寻找本机方法克隆的实现细节 任何指示将不胜感激 请注意 我知道克隆的缺点 protected native Object clone 我不太清楚 我需要查看本机代
  • 从 ArrayList HashMap 中获取多个随机值

    我想从 ArrayList 中获取一些特定数字的随机值 final ArrayList
  • 如何将一个组件放在其他组件之上?

    我有一个JScrollPanel其中包括一个大面板 其本身包括 3 个内面板 我想将一个面板 例如 放在一个特殊的位置 以便始终可以看到 我的意思是用户可以滚动到想要的任何地方 但该面板始终位于其他组件的顶部并且不会移动 我试图通过这样做J
  • Android Edittext Onclick Datepickerdialog 棒棒糖中出现错误

    我正在使用日期选择器对话框 它在 kitkat 上运行正常 但是当我在棒棒糖上运行应用程序时 当我单击编辑文本时 它会打开一个日期选择器对话框 但当我选择日期时 它会不幸地给出停止错误 以下是 edittext 上日期选择器的代码 priv
  • 创建 FileInputStream 对象时使用 InputStream 而不是 FileInputStream 有什么区别

    这可能很愚蠢 但我想知道后台操作的区别 InputStream is new FileInputStream filepath FileInputStream is new FileInputStream filepath 上面两行代码有什
  • 将私有 Java 9 模块包公开给 JUnit 的正确方法是什么?

    我有一个 可执行 Java 9 模块 意味着它不会公开任何包 它只包含一个main函数 我需要测试 我正在使用 Gradle 的java library and org gradle java experimental jigsaw插件 我
  • Java:如何实现通用二叉搜索树?

    到目前为止 我一直在编写一个 Node 类 class Node private value private Node left private Node right public int getValue return value pub
  • Apache PDFBox 旋转 PDImageXObject

    我正在使用 2 0 0 SNAPSHOT 我想将页面设置为横向并旋转我的图片 所以我已经做到了page setRotation 90 使用 PDFBox 和 AffineTransform 似乎存在错误 这段代码没有做任何我期望的事情 Af
  • 如何在 Tomcat 6 中合理配置安全策略

    我使用的是为 Ubuntu Karmic 打包的 Tomcat 6 0 24 Ubuntu 的 Tomcat 软件包的默认安全策略相当严格 但看起来很简单 在 var lib tomcat6 conf policy d 有多种建立默认策略的
  • 如何在mockito中模拟Spring依赖

    我正在尝试嘲笑 Spring Beans 我能够模拟对象 B 和 C 但无法模拟 B 类内的对象 插入类 A 中的模拟包含 B 但 X 和 Y 为空 即使我嘲笑了它们 Mockito 有没有办法模拟 Spring bean 中成员的对象 N

随机推荐

  • 如何在 Linux 中搜索最近修改的文件

    本教程将帮助您通过命令行在 Linux 中查找最近修改的文件 find 命令允许我们以分钟或天为单位定义持续时间 分钟定义为 mmin天数可以定义为 mtime 您还可以定义搜索条件来查找在指定时间内或之前修改的文件 例如 要搜索之前修改过
  • 如何安装Go 1.20 CentOS/RHEL 9/8

    Go是一种开源编程语言 由以下团队开发Google 它提供了易于构建的简单 可靠且高效的软件 这种语言是为编写服务器而设计的 这就是它如今被广泛使用的原因 Go最近发布了最新版本1 20 本教程将帮助您在 CentOS 和 RHEL 9 8
  • 如何备份/恢复 MySQL 存储过程和触发器

    存储过程 and Triggers首先是在 MySQL 5 0 中引入的 因此 如果您仍在使用 MySQL 旧版本 请将其升级到 MySQL 5 0 或更高版本以使用这些功能 本文将帮助您了解如何使用以下命令转储存储过程和触发器 mysql
  • 兆字节 (MiB):了解基础知识和优点

    在当今的数字世界中 文件大小和存储容量已成为我们日常生活中不可或缺的一部分 无论我们是下载文件 上传数据 还是只是管理我们的设备 了解用于测量数字存储的单位都至关重要 Mebibyte MiB 就是这样的单位之一 由于它与更广为人知的兆字节
  • 在 CentOS 7 上使用 Let's Encrypt 保护 Nginx

    Let s Encrypt 是由互联网安全研究小组 ISRG 开发的免费开放的证书颁发机构 如今 Let s Encrypt 颁发的证书几乎受到所有浏览器的信任 在本教程中 我们将提供有关如何在 CentOS 7 上使用 certbot 工
  • 如何在 Debian 9 上安装 Ruby

    本教程将引导您完成在 Debian 9 系统上安装 Ruby 的步骤 Ruby 是当今最流行的语言之一 它具有优雅的语法 并且是强大的 Ruby on Rails 框架背后的语言 在 Debian 上安装 Ruby 有多种不同的方法 在以下
  • 如何在 Ubuntu 20.04 上安装 Mono

    Mono 是一个用于开发和运行基于 ECMA ISO 标准的跨平台应用程序的平台 它是 Microsoft NET 框架的免费开源实现 本教程介绍了在 Ubuntu 20 04 上安装 Mono 所需的步骤 先决条件 这些说明假定您以 ro
  • 如何在 CentOS 7 上安装 Django

    Django 是一个免费开源的高级 Python Web 框架 旨在帮助开发人员构建安全 可扩展和可维护的 Web 应用程序 有不同的方法来安装 Django 具体取决于您的需要 它可以在系统范围内安装 也可以使用 pip 安装在 Pyth
  • 如何在 Ubuntu 20.04 上安装 Anaconda

    Anaconda 是一个流行的 Python R 数据科学和机器学习平台 用于大规模数据处理 预测分析和科学计算 Anaconda 发行版附带 250 个开源数据包 并且可以从 Anaconda 存储库安装超过 7 500 个附加包 它还包
  • 如何在 Python 中将整数转换为字符串

    Python 有多种内置数据类型 有时 在编写 Python 代码时 您可能需要将一种数据类型转换为另一种数据类型 例如 连接一个字符串和整数 首先 您需要将整数转换为字符串 本文介绍如何将 Python 整数转换为字符串 Python s
  • 如何重置 MySQL 根密码

    您是否忘记了 MySQL root 密码 别担心 这发生在我们所有人身上 在本文中 我们将向您展示如何从命令行重置 MySQL root 密码 识别服务器版本 根据您系统上运行的 MySQL 或 MariaDB 服务器版本 您将需要使用不同
  • Bash printf 命令

    通常 在编写 bash 脚本时 我们使用echo打印到标准输出 echo是一个简单的命令 但其功能有限 要更好地控制输出的格式 请使用printf命令 The printf命令格式并打印其参数 类似于 Cprintf 功能 printf命令
  • 如何在 CentOS 7 上安装 Jenkins

    Jenkins是一个基于 Java 的开源自动化服务器 提供了一种设置持续集成和持续交付 CI CD 管道的简单方法 持续集成 CI 是一种 DevOps 实践 团队成员定期将代码更改提交到版本控制存储库 然后运行自动化构建和测试 持续交付
  • Linux 中的 Usermod 命令

    usermod是一个命令行实用程序 允许您修改用户的登录信息 本文介绍了如何使用usermod命令将用户添加到组 更改用户 shell 登录名 主目录等 usermod命令 的语法usermod命令采用以下形式 usermod option
  • 如何在 Ubuntu 18.04 上使用 UFW 设置防火墙

    正确配置的防火墙是整个系统安全最重要的方面之一 默认情况下 Ubuntu附带了一个名为UFW Uncomplicated Firewall 的防火墙配置工具 UFW 是一个用户友好的前端 用于管理 iptables 防火墙规则 其主要目标是
  • Python 模运算符

    模运算是一种算术运算 可求出一个数字除以另一个数字的余数 余数称为运算的模 例如 5除以3等于1 余数为2 8除4等于2 余数为0 Python 模运算符 在 Python 中 模运算符由百分号 语法如下 num1 num2 这是一个例子
  • Linux服务器上重置Mysql8密码

    前言 此流程适用于mysql 8版本 1 关闭数据库 1 关闭数据库 service mysqld stop 2 编辑配置文件 1 编辑文件 vim etc my cnf 输入 i 进入编辑模式 2 添加配置 skip grant tabl
  • ECSHOP文件结构系统简介

    原来做电子商务系统一直用zencart 后来虽然接触过一段时间magento 但是magento觉得还是挺高深的 前面两个比较多的用在外贸电子商务 特别是magento 可以说是开源电子商务系统中的豪华版 以后有时间的话再一起学习探讨一下
  • 通过App的演示深入理解区块链运行原理

    下载安装 如果没有安装nodejs 需要先安装 nodejs Clone this repository git clone https github com seanseany blockchain cli Go into the rep
  • 源码进阶之线程池

    写在前面 上次学习了多线程 了解了线程的概念和作用 学习了线程的创建方式 工作模式和一些重要的方法 当我们使用线程中 创建 销毁线程伴随着系统开销 过于频繁的创建 销毁线程 就会很大程度上影响处理效率 那么此时我们就引入了线程池的概念 即为