详解 ThreadPoolExecutor 的参数含义及源码执行流程?

Java 学习+面试指南:https://javaxiaobear.cn

线程池是为了避免线程频繁的创建和销毁带来的性能消耗,而建立的一种池化技术,它是把已创建的线程放入“池”中,当有任务来临时就可以重用已有的线程,无需等待创建的过程,这样就可以有效提高程序的响应速度。但如果要说线程池的话一定离不开 ThreadPoolExecutor ,在阿里巴巴的《Java 开发手册》中是这样规定线程池的:

线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的读者更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

  • CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

其实当我们去看 Executors 的源码会发现,Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor() 和 Executors.newCachedThreadPool() 等方法的底层都是通过 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,且必须大于 corePoolSizemaximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.acc = System.getSecurityManager() == null ?null :AccessController.getContext();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 表示线程的创建工厂,此参数一般用的比较少,我们通常在创建线程池时不指定此参数,它会使用默认的线程创建工厂的方法来创建线程,源代码如下:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue) {// Executors.defaultThreadFactory() 为默认的线程创建工厂this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);
}
public static ThreadFactory defaultThreadFactory() {return new DefaultThreadFactory();
}
// 默认的线程创建工厂,需要实现 ThreadFactory 接口
static class DefaultThreadFactory implements ThreadFactory {private static final AtomicInteger poolNumber = new AtomicInteger(1);private final ThreadGroup group;private final AtomicInteger threadNumber = new AtomicInteger(1);private final String namePrefix;DefaultThreadFactory() {SecurityManager s = System.getSecurityManager();group = (s != null) ? s.getThreadGroup() :Thread.currentThread().getThreadGroup();namePrefix = "pool-" +poolNumber.getAndIncrement() +"-thread-";}// 创建线程public Thread newThread(Runnable r) {Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0);if (t.isDaemon()) t.setDaemon(false); // 创建一个非守护线程if (t.getPriority() != Thread.NORM_PRIORITY)t.setPriority(Thread.NORM_PRIORITY); // 线程优先级设置为默认值return t;}
}

我们也可以自定义一个线程工厂,通过实现 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 作为阀值。

考点分析

这道面试题考察的是你对于线程池和 ThreadPoolExecutor 的掌握程度,也属于 Java 的基础知识,几乎所有的面试都会被问到,其中线程池任务执行的主要流程,可以参考以下流程图:

与 ThreadPoolExecutor 相关的面试题还有以下几个:

  • ThreadPoolExecutor 的执行方法有几种?它们有什么区别?

  • 什么是线程的拒绝策略?

  • 拒绝策略的分类有哪些?

  • 如何自定义拒绝策略?

  • ThreadPoolExecutor 能不能实现扩展?如何实现扩展?

知识扩展

execute() VS submit()

execute() 和 submit() 都是用来执行线程池任务的,它们最主要的区别是,submit() 方法可以接收线程池执行的返回值,而 execute() 不能接收返回值。

来看两个方法的具体使用:

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 10L,TimeUnit.SECONDS, new LinkedBlockingQueue(20));
// execute 使用
executor.execute(new Runnable() {@Overridepublic void run() {System.out.println("Hello, execute.");}
});
// submit 使用
Future<String> future = executor.submit(new Callable<String>() {@Overridepublic String call() throws Exception {System.out.println("Hello, submit.");return "Success";}
});
System.out.println(future.get());

以上程序执行结果如下:

Hello, submit.
Hello, execute.
Success

从以上结果可以看出 submit() 方法可以配合 Futrue 来接收线程执行的返回值。它们的另一个区别是 execute() 方法属于 Executor 接口的方法,而 submit() 方法则是属于 ExecutorService 接口的方法,它们的继承关系如下图所示:

线程池的拒绝策略

当线程池中的任务队列已经被存满,再有任务添加时会先判断当前线程池中的线程数是否大于等于线程池的最大值,如果是,则会触发线程池的拒绝策略。

Java 自带的拒绝策略有 4 种:

  • AbortPolicy,终止策略,线程池会抛出异常并终止执行,它是默认的拒绝策略;

  • CallerRunsPolicy,把任务交给当前线程来执行;

  • DiscardPolicy,忽略此任务(最新的任务);

  • DiscardOldestPolicy,忽略最早的任务(最先加入队列的任务)。

例如,我们来演示一个 AbortPolicy 的拒绝策略,代码如下:

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 3, 10,TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),new ThreadPoolExecutor.AbortPolicy()); // 添加 AbortPolicy 拒绝策略
for (int i = 0; i < 6; i++) {executor.execute(() -> {System.out.println(Thread.currentThread().getName());});
}

以上程序的执行结果:

pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.lagou.interview.ThreadPoolExample$$Lambda$1/1096979270@448139f0 rejected from java.util.concurrent.ThreadPoolExecutor@7cca494b[Running, pool size = 3, active threads = 3, queued tasks = 2, completed tasks = 0]at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)at com.lagou.interview.ThreadPoolExample.rejected(ThreadPoolExample.java:35)at com.lagou.interview.ThreadPoolExample.main(ThreadPoolExample.java:26)

可以看出当第 6 个任务来的时候,线程池则执行了 AbortPolicy 拒绝策略,抛出了异常。因为队列最多存储 2 个任务,最大可以创建 3 个线程来执行任务(2+3=5),所以当第 6 个任务来的时候,此线程池就“忙”不过来了。

自定义拒绝策略

自定义拒绝策略只需要新建一个 RejectedExecutionHandler 对象,然后重写它的 rejectedExecution() 方法即可,如下代码所示:

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 3, 10,TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),new RejectedExecutionHandler() {  // 添加自定义拒绝策略@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {// 业务处理方法System.out.println("执行自定义拒绝策略");}});
for (int i = 0; i < 6; i++) {executor.execute(() -> {System.out.println(Thread.currentThread().getName());});
}

以上代码执行的结果如下:

执行自定义拒绝策略
pool-1-thread-2
pool-1-thread-3
pool-1-thread-1
pool-1-thread-1
pool-1-thread-2

可以看出线程池执行了自定义的拒绝策略,我们可以在 rejectedExecution 中添加自己业务处理的代码。

ThreadPoolExecutor 扩展

ThreadPoolExecutor 的扩展主要是通过重写它的 beforeExecute() 和 afterExecute() 方法实现的,我们可以在扩展方法中添加日志或者实现数据统计,比如统计线程的执行时间,如下代码所示:

public class ThreadPoolExtend {public static void main(String[] args) throws ExecutionException, InterruptedException {// 线程池扩展调用MyThreadPoolExecutor executor = new MyThreadPoolExecutor(2, 4, 10,TimeUnit.SECONDS, new LinkedBlockingQueue());for (int i = 0; i < 3; i++) {executor.execute(() -> {Thread.currentThread().getName();});}}/*** 线程池扩展*/static class MyThreadPoolExecutor extends ThreadPoolExecutor {// 保存线程执行开始时间private final ThreadLocal<Long> localTime = new ThreadLocal<>();public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}/*** 开始执行之前* @param t 线程* @param r 任务*/@Overrideprotected void beforeExecute(Thread t, Runnable r) {Long sTime = System.nanoTime(); // 开始时间 (单位:纳秒)localTime.set(sTime);System.out.println(String.format("%s | before | time=%s",t.getName(), sTime));super.beforeExecute(t, r);}/*** 执行完成之后* @param r 任务* @param t 抛出的异常*/@Overrideprotected void afterExecute(Runnable r, Throwable t) {Long eTime = System.nanoTime(); // 结束时间 (单位:纳秒)Long totalTime = eTime - localTime.get(); // 执行总时间System.out.println(String.format("%s | after | time=%s | 耗时:%s 毫秒",Thread.currentThread().getName(), eTime, (totalTime / 1000000.0)));super.afterExecute(r, t);}}
}

以上程序的执行结果如下所示:

pool-1-thread-1 | before | time=4570298843700
pool-1-thread-2 | before | time=4570298840000
pool-1-thread-1 | after | time=4570327059500 | 耗时:28.2158 毫秒
pool-1-thread-2 | after | time=4570327138100 | 耗时:28.2981 毫秒
pool-1-thread-1 | before | time=4570328467800
pool-1-thread-1 | after | time=4570328636800 | 耗时:0.169 毫秒

总结

最后我们总结一下:线程池的使用必须要通过 ThreadPoolExecutor 的方式来创建,这样才可以更加明确线程池的运行规则,规避资源耗尽的风险。同时,也介绍了 ThreadPoolExecutor 的七大核心参数,包括核心线程数和最大线程数之间的区别,当线程池的任务队列没有可用空间且线程池的线程数量已经达到了最大线程数时,则会执行拒绝策略,Java 自动的拒绝策略有 4 种,用户也可以通过重写 rejectedExecution() 来自定义拒绝策略,我们还可以通过重写 beforeExecute() 和 afterExecute() 来实现 ThreadPoolExecutor 的扩展功能。

在这里插入图片描述

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

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

相关文章

开源C语言库Melon:多线程治理

问题描述 不知你是否有过类似如下的需求&#xff1a; 有一些功能&#xff0c;它们足够单一&#xff0c;但又需要后台持续运行&#xff0c;以容器实现感觉太重了&#xff0c;以进程实现又太琐碎了&#xff0c;以线程实现可以接受但是又不好管理。 这类程序诸如&#xff1a;数据…

详解c++移动构造函数和移动赋值运算符在代码性能中起的作用

对象移动 对象移动&#xff0c;就是把一个不想用了的对象A中的一些有用的数据提取出来&#xff0c;在构建新对象B的时候就不需要重新构建对象中的所有数据——从不想用了的对象A中提取出来的有用数据在构建对象B时都可以拿来使用。 我们知道&#xff0c;拷贝构造函数、拷贝赋…

Java中什么是多线程?

Java是一种支持多线程编程的编程语言&#xff0c;它提供了内置的多线程支持&#xff0c;使得开发者能够创建并发执行的程序。多线程是一种在同一程序中同时执行多个线程的机制&#xff0c;每个线程都是独立运行的&#xff0c;并且可以共享相同的资源。在Java中&#xff0c;多线…

设计模式之观察者模式【行为型模式】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档> 学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某…

把图表题注编号由“0.1”改为“1.1“ (方法二)

前置设置&#xff1a; 手打章节标题&#xff0c;“第一章 绪论”&#xff0c;“第二章 相关理论和技术方法”。给章节标题设置样式 “标题一”&#xff0c;设置为一级标题。打开导航窗格&#xff0c;可以不开&#xff0c;我纯粹是为了操作方便。直接先给图片插入题注&#xff…

cpolar 内网穿透 使用

cpolar 内网穿透 使用 官网地址&#xff1a;https://www.cpolar.com/ 官网文档&#xff1a;https://www.cpolar.com/blog/cpolar-quick-start-tutorial-centos-series 获取隧道Authtoken&#xff1a;https://dashboard.cpolar.com/auth 步骤 1、先去注册 在这个地方注册&…

重磅2023年度openGauss标杆应用实践案例正式揭晓

12月28日&#xff0c;在openGauss Summit 2023峰会上&#xff0c;正式揭晓了“2023年度openGauss标杆应用实践案例”。 数据库作为企业IT系统的核心组成部分&#xff0c;是数字基础设施建设的关键&#xff0c;也是实现数据安全稳定的保障。为更好地推动产业技术创新&#xff0…

使用Linux防火墙管理HTTP流量

在Linux系统中&#xff0c;防火墙是用于控制网络流量的重要工具。通过防火墙&#xff0c;你可以根据需要限制、过滤或允许特定的网络流量&#xff0c;从而提高系统的安全性。在处理HTTP流量时&#xff0c;防火墙可以帮助你实施访问控制、流量监控和其他安全策略。 iptables i…

PPT插件-大珩助手-免费功能-特殊格式介绍

上、下标切换 直接切换选中的字符为上、下标。 大小金额 支持超大金额的大写金额转换 当前日期 本次打开文件的时间 转二维码 将当前选中的文字&#xff0c;转为二维码图片&#xff0c;并插入到PPT当前位置 特殊字符 内置常用的特殊字符&#xff0c;点击使用 软件介绍 …

Java多线程并发篇----第五篇

系列文章目录 文章目录 系列文章目录前言一、Java中interrupted 和 isInterruptedd方法的区别?二、Java中synchronized 和 ReentrantLock 有什么不同?三、有三个线程T1,T2,T3,如何保证顺序执行?前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分…

关于CodeReview的一些实践和思考

在日常开发中&#xff0c;Code Review 的重要性日益凸显。它不仅有助于提升代码质量&#xff0c;还促进了团队成员之间的知识共享和技能提升。本文将主要聚焦于 Code Review&#xff0c;分享在这个过程中的一些心得和思考。 CodeReview常用到的一些术语 之前看到公司的大佬经…

autoxjs 安卓爬虫自动化

autoxjs 安卓爬虫自动化 我这里只是测试请勿用于违法的 我这里是小红书 文章目录 autoxjs 安卓爬虫自动化前言一、自动刷直播间并且抓取商品已经粉丝数量等&#xff1f;总结 前言 欢迎来到AutoXJS的世界&#xff0c;这是一个充满创新、挑战和技术探索的领域。在这个引领未来的…

基于Springboot+vue高校宿舍管理系统(前后端分离)

该项目完全免费 高校宿舍管理系统采用前后端分离的架构方式&#xff0c;是为学校宿舍管理打造的一套系统,可以让管理者更为便捷地处理学生公寓问题,从而大大提高管理效率,让学生公寓的资源合理分配,事半功倍,进而改善了学生公寓管理。 系统分为三种角色&#xff0c;分别是系统…

小巧且兼具高性能的小模型 TinyLlama 等

TinyLlama-1.1B 小模型在边缘设备上有着广泛的应用&#xff0c;如智能手机、物联网设备和嵌入式系统&#xff0c;这些边缘设备通常具有有限的计算能力和存储空间&#xff0c;它们无法有效地运行大型语言模型。因此&#xff0c;深入探究小型模型显得尤为重要。 来自新加坡科技…

[ 机器学习 ] 关于Jupyter Notebook中pytorch模块import失败的问题

0x01、问题描述 在使用WSL搭建Jupyter进行代码测试的时候 发现Miniconda&#xff08;虚拟环境均适用&#xff09;中安装的pytorch在Jupyter里面import失败 但在python解释器的命令模式里可以测试import成功 并且torch.cuda_available()打印True 以前用的是IDEA没怎么用Jup…

Navicat迁移局域网内其他PC机的MySQL数据库

迁移局域网内其他PC机的MySQL数据库到本机 查看局域网IP 设置可远程连接的账号 开放本机防火墙的3306端口 连接PC机的MySQL 利用Navicat迁移数据库 刚换了个电脑&#xff0c;旧电脑的MySQL数据库太多了&#xff0c;转成.sql文件&#xff0c;再传输到新电脑上运行&#xff…

CSS3新增边框样式

边框样式 概念:在CSS3中&#xff0c;针对元素边框增加了丰富的修饰属性。 常见的边框样式属性有以下 属性说明border-radius圆角效果box-shadow边框阴影border-image边框背景 border-radius属性 概念&#xff1a;border-radius属性可以为元素添加圆角效果 语法&#xff1…

盘点五大设备巡检系统!内附巡检注意事项及巡检要点

在繁忙的工业生产现场&#xff0c;设备高效运行是保证生产顺利开展的关键。为了确保设备的稳定性和生产的安全性&#xff0c;设备巡检工作变得尤为重要。 举个通俗易懂的例子—— 你开了一家钢铁制造厂&#xff0c;工厂拥有多条自动化生产线&#xff0c;每天24小时不间断地运…

运用tomcat在浏览器中对数据库信息进行查询

在idea中创建好项目后&#xff0c;添加web项目 然后打开idea的setting&#xff0c;跳转到下面的页面&#xff0c;下载maven插件。 出现下面的选项&#xff0c;才正确。 添加好web项目后&#xff0c;打开pom文件&#xff0c;添加相应的依赖&#xff1a; <?xml version"…

golang中的循环依赖

作为 Golang 开发人员&#xff0c;您可能遇到过导入周期。Golang 不允许导入循环。如果 Go 检测到代码中的导入循环&#xff0c;则会抛出编译时错误。在这篇文章中&#xff0c;让我们了解导入周期是如何发生的以及如何处理它们。 导入周期 假设我们有两个包&#xff0c;p1并且…