详细剖析Java动态线程池的扩容以及缩容操作
前言
在项目中,我们经常会使用到线程来处理加快我们的任务。但为了节约资源,大多数程序员都会把线程进行池化,使用线程池来更好的支持我们的业务。
Java线程池ThreadPoolExecutor
有几个比较核心的参数,如corePoolSize、maximumPoolSize
等等。无论是在工作中还是在面试中,都会被问到,如何正确的设置这几个参数。
线程池的参数并不好配置。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识。项目IO密集型还是CPU密集型等等,总归很难确定一个完美的参数,此时就有了动态线程池的诞生。
动态线程池(DTP)原理
其实动态线程池并不是很高大上的技术,它底层依旧是依赖了ThreadPoolExecutor
的一些核心接口方法。我们通过下面图片可以很清楚的看到,ThreadPoolExecutor
本身就给我们提供了很多钩子方法,让我们去定制化。
那么其原理也非常简单了,我们在运行中假设有一个线程池叫做TaskExecutor
- 他的核心线程池默认假设是10,现在我发觉不够用了,此时我想把他的核心线程池调整为20
- 我可以写一个远程配置(可以阿波罗,zk,redis什么都可以)。然后监听到了这个配置变为了core.pool.size=20
- 然后我获取到了这个线程池
TaskExecutor
,并且调用setCorePoolSize(20)
,那么这个TaskExecutor
核心线程数就变为了20
就是这么简单,拨开表面,探究原理,内部其实非常的简单。当时公司里面的线程池还有加一些友好的界面、监控告警、操作日志、权限校验、审核等等,但本质就是监听配置,然后调用setCorePoolSize方法去实现的,最大线程数类似。
public void setCorePoolSize(int corePoolSize) {
if (corePoolSize < 0)
throw new IllegalArgumentException();
int delta = corePoolSize - this.corePoolSize;
this.corePoolSize = corePoolSize;
if (workerCountOf(ctl.get()) > corePoolSize)
interruptIdleWorkers();
else if (delta > 0) {
int k = Math.min(delta, workQueue.size());
while (k-- > 0 && addWorker(null, true)) {
if (workQueue.isEmpty())
break;
}
}
}
动态线程池缩容
首先提出几个问题
- 核心线程数为5,现在有3个线程在执行,并且没有执行完毕,我修改核心线程数为4,是否修改成功
- 核心线程数为5,现在有3个线程在执行,并且没有执行完毕,我修改核心线程数为1,是否修改成功
让我们带着疑问去思考问题。
- 首先第一个问题,因为核心线程池数为5,仅有3个在执行,我修改为4,那么因为有2个空闲的线程,它只需要销毁1个空闲线程即可,因此是成功的
- 第二个问题,核心线程池数为5,仅有3个在执行,我修改为1。虽然有2个空闲线程,但是我需要销毁4个线程。因为有2个空闲线程,2个非空闲线程。我只能销毁2个空闲线程,另外2个执行的任务不能被打断,也就是执行后仍然为3个核心线程数。
- 那什么时候销毁剩下2个执行的线程呢,等到2个执行的任务完毕之后,就会销毁它了。假设这个任务是一个死循环,永远不会结束,那么核心线程数永远是3,永远不能设置为1
我们举一个代码的例子如下
ThreadPoolExecutor es = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
es.prestartAllCoreThreads(); // 预启动所有核心线程
// 启动三个任务,执行次数不一样
for (int i = 0; i < 3; i++) {
int finalI = i;
es.execute(() -> {
int cnt = 0;
while (true) {
try {
cnt++;
TimeUnit.SECONDS.sleep(2);
if (cnt > finalI + 1) {
log.info(Thread.currentThread().getName() + " 执行完毕");
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
TimeUnit.SECONDS.sleep(1); // 等待线程池中的线程执行
log.info("修改前 es = {}", es); // 这里核心线程数必定是5
es.setCorePoolSize(1); // 修改核心线程数为1,但是核心线程数为5,并且有3个线程在执行任务,
while (true) {
TimeUnit.SECONDS.sleep(1); // 等待
log.info("修改后 es = {}", es);
}
输出结果为如下
// 修改前核心线程数为5,运行线程数为3
[修改前 es = java.util.concurrent.ThreadPoolExecutor@72d818d1[Running, pool size = 5, active threads = 3, queued tasks = 0, completed tasks = 0]]
// 因为有2个空闲线程,先把2个空闲线程给销毁了,剩下3个线程
[修改后 es = java.util.concurrent.ThreadPoolExecutor@72d818d1[Running, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 0]]
// 等第1个任务执行完毕,剩下2个线程
[Main.lambda$d$0:38] [pool-2-thread-1 执行完毕]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@72d818d1[Running, pool size = 2, active threads = 2, queued tasks = 0, completed tasks = 1]]
// 等第2个任务执行完毕,剩下1个线程
[Main.lambda$d$0:38] [pool-2-thread-2 执行完毕]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@72d818d1[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 2]]
// 等第3个任务执行完毕,剩下1个线程。因为我修改的就是1个核心线程
[Main.lambda$d$0:38] [pool-2-thread-3 执行完毕]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@72d818d1[Running, pool size = 1, active threads = 0, queued tasks = 0, completed tasks = 3]]
有兴趣的读者可以拿这块带去自己去试试,输出结果里面的注释 我写的非常详细,大家可以详细品品这块输出结果。
动态线程池扩容
扩容我就不提问问题了,和缩容异曲同工,但我希望读者可以先看下以下代码,不要看答案,认为会输出什么结果,看下是否和自己想的是否一样,如果一样,那说明你已经完全懂了,如果不一样,是什么原因。
// 核心线程数1,最大线程数10
ThreadPoolExecutor es = new ThreadPoolExecutor(1, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
es.prestartAllCoreThreads(); // 预启动所有核心线程
for (int i = 0; i < 5; i++) {
int finalI = i;
es.execute(() -> {
int cnt = 0;
while (true) {
try {
cnt++;
TimeUnit.SECONDS.sleep(2);
if (cnt > finalI + 1) {
log.info(Thread.currentThread().getName() + " 执行完毕");
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
TimeUnit.SECONDS.sleep(1); // 等待线程池中的线程执行
log.info("修改前 es = {}", es); // 这里核心线程数必定是1, 队列里面有4个任务
es.setCorePoolSize(3); // 修改核心线程数为3
while (true) {
TimeUnit.SECONDS.sleep(1); // 等待
log.info("修改后 es = {}", es);
}
输出结果为如下 (注意观察输出queued tasks的变化!!!)
[修改前 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 1, active threads = 1, queued tasks = 4, completed tasks = 0]]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 3, active threads = 3, queued tasks = 2, completed tasks = 0]]
[Main.lambda$a$1:73] [pool-2-thread-1 执行完毕]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 1]]
[Main.lambda$a$1:73] [pool-2-thread-2 执行完毕]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 2]]
[Main.lambda$a$1:73] [pool-2-thread-3 执行完毕]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 3, active threads = 2, queued tasks = 0, completed tasks = 3]]
[Main.lambda$a$1:73] [pool-2-thread-1 执行完毕]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 3, active threads = 1, queued tasks = 0, completed tasks = 4]]
[Main.lambda$a$1:73] [pool-2-thread-2 执行完毕]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 3, active threads = 0, queued tasks = 0, completed tasks = 5]]
最后
在业务中,我们为了提高效率使用了线程,为了加快线程我们使用了线程池,而又为了更好的利用线程池的资源,我们又实现了动态化线程池。这也就是遇到问题、探索问题、解决问题的一套思路吧。
我们从底层原理分析,发现动态线程池的底层原理非常简单,希望大家不要恐惧,往往拨开外衣,发现里面最根本的原理,才能是我们更好的捋清楚其中的逻辑。希望本文提供的动态化线程池思路能对大家有帮助。
最终也极力希望读者朋友们,可以将上述两个例子详细分析一下原因,相信会有不小的进步,谢谢大家。
详细剖析Java动态线程池的扩容以及缩容操作的更多相关文章
- java动态线程池LinkedBlockingQueue和SynchronousQueue比较
import java.util.concurrent.Callable; public class MyCallable implements Callable<String> { pr ...
- 深入理解Java之线程池
原作者:海子 出处:http://www.cnblogs.com/dolphin0520/ 本文归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则 ...
- 深入理解Java之线程池(爱奇艺面试)
爱奇艺的面试官问 (1) 线程池是如何关闭的 (2) 如何确定线程池的数量 一.线程池销毁,停止线程池 ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown() ...
- [转]深入理解Java之线程池
原文链接 原文出处: 海 子 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这 ...
- Java并发--线程池的使用
在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统 ...
- Java之线程池(一)
在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统 ...
- Java进阶——— 线程池的原理分析
前言 在了解线程池之前,其实首先出现的疑问是:为什么要使用线程池,其次是了解什么是线程池,最后是如何使用线程池,带着疑问去学习. 为什么要使用 前面多线程文章中,需要使用线程就开启一个新线程,简单方便 ...
- Java ThreadPoolExecutor线程池原理及源码分析
一.源码分析(基于JDK1.6) ThreadExecutorPool是使用最多的线程池组件,了解它的原始资料最好是从从设计者(Doug Lea)的口中知道它的来龙去脉.在Jdk1.6中,Thread ...
- 动态线程池(DynamicTp)之动态调整Tomcat、Jetty、Undertow线程池参数篇
大家好,这篇文章我们来介绍下动态线程池框架(DynamicTp)的adapter模块,上篇文章也大概介绍过了,该模块主要是用来适配一些第三方组件的线程池管理,让第三方组件内置的线程池也能享受到动态参数 ...
- Java中线程池的学习
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理.当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程 ...
随机推荐
- HarmonyOS Next 入门实战 - 创建项目、主题适配
开发一个简单的demo,其中涉及一些鸿蒙应用开发的知识点,其中涉及导航框架,常用组件,列表懒加载,动画,深色模式适配,关系型数据库等内容,在实践中学习和熟悉鸿蒙应用开发. 首先下载并安装 ...
- jQuery 元素信息
先贴出元素模型信息 1.获取内容区大小 css():返回值是带单位的(getComputedStyle(node).width) <script> $(function(){ consol ...
- 服务拆分之《阿里云OCR使用指南》
在做一件什么事情: 遇到了什么问题: 问题分析: 业界解决方案: 我的方案: 最终的结果: 服务都已经迁移过来了,对应的那些使用的工具什么的也都得换成自己的账号.起初原始用的是什么忘记了,时间太长了已 ...
- P11378[GESP202412 七级]燃烧 题解
闲话 花了一个小时. 主要原因:条初始值硬控我半小时,题目看错硬控我半小时(悲). 正文 看题目,就是求从哪个点出发所得到的所有单调下降序列的总长度最长(这个描述好奇怪,不过意思是对的). 题目中说的 ...
- 金TECH频道|最近备受关注的应用重构,到底怎么做?
"金TECH频道"旨在为您分享中电金信助力行业数字化转型的最新产品业务动态.技术观点洞察与应用实践案例.让我们在这里,与行业发展同频共振,共筑数字新基石.
- 【前端】CSS实现图片文字对齐 并随着设备尺寸改变而改变大小
效果预览 HTML源码 点击查看HTML代码 <!DOCTYPE html> <html lang="zh-cn"> <head> <me ...
- Jackson ObjectMapper - 指定对象属性的序列化顺序
注释很有用,但在任何地方应用起来都会很痛苦.您可以配置整个 ObjectMapper 以这种方式工作 当前杰克逊版本: objectMapper.configure(MapperFeature.SOR ...
- Qt开发经验小技巧256-260
默认QDialog窗体右下角有个拉伸尺寸的手柄,通过它可以对窗体拉伸大小,这个控件很容易被遗忘但是又经常可以看到,他的名字叫QSizeGrip,可以通过setSizeGripEnabled来启用或者禁 ...
- Qt编写物联网管理平台33-设备面板
一.前言 设备面板展示数据,相对于表格展示,可能在一个页面中能够展示的设备数据量少一些,但是有些用户和场景,又需要这种面板的形式,可能更生动形象一些.尤其是经过这么些年的社会的毒打,我的原则是:用户是 ...
- Qt编写安防视频监控系统60-子模块4云台控制
一.前言 云台控制是一个很老很基础的模块了,视频监控系统中必备的特殊模块之一,可以对选中的通道的摄像机(ONVIF协议),进行云台控制,可以控制球机的上下左右等各个方位的移动,还可以调节变倍步长,至于 ...