一、为什么要学习线程池最佳线程数的确定
系统线程池的最佳核心线程、最大线程数的确定,是Java开发人员最常遇到的一个技术难题。
为什么呢?
简单来说,配置得当,你的系统性能就能蹭蹭蹭地提升;
配置不当,不仅浪费资源,还可能拖慢整个系统,甚至引发各种奇怪的bug
合理配置线程池有很多讲究,涉及到各种不同的场景和需求:
有时候,系统是IO密集型的,这时候就需要多一些线程来弥补IO等待时间
有时候,系统是CPU密集型的,这时候就需要控制线程数,以避免过多的上下文切换
有时候,系统是混合型的,需要综合考虑各方面的需求......
在这里,小北会一步步拆解,从理论预估到压测验证,再到线上调整,最后还会结合Nacos和PGA,手把手教你实现动态化的线程池管理。
目标就是让你在不管在面试过程中,还是日常系统开发中,都能够游刃有余的应对。
真的免费,如果你近期准备面试跳槽,建议在cxykk.com在线刷题,涵盖 1万+ 道 Java 面试题,几乎覆盖了所有主流技术面试题、简历模板、算法刷题
二、如何确定系统的最佳线程数?
啥是系统的最佳线程数呢?
简单来说,就是在不浪费系统资源的前提下,让你的应用跑得最快、最稳的那个线程数。
线程数太少,任务处理不过来,系统性能就上不去;线程数太多,又会导致资源争夺,反而拖慢系统。
这就像你家厨房做饭,人少了忙不过来,人多了反而碍事。
确定最佳线程数的基本思路和方法
怎么确定这个"最佳线程数"呢?有没有一套成熟的套路来帮助我们快速确定呢?
答案是当然有:
大体上,可以分为三步:
-
第一步、理论预估
-
第二步、压测验证
-
第三步、监控动态调整
三、确定系统的最佳线程数的3个步骤
3.1、线程数的理论预估(设计阶段)
首先、需要确定我们系统的任务类型。 大体上,可以分为三类:IO密集型 、CPU密集型 和混合型
- IO密集型任务:
比如读写文件、网络通信等。这种任务大部分时间都在等IO操作完成,所以CPU闲着也是闲着。这种情况就需要多开点线程,让CPU能干别的事,不至于浪费。
- CPU密集型任务:
比如复杂的计算、数据处理等。这种任务会把CPU忙个不停,线程多了反而会抢资源,导致频繁的上下文切换。一般来说,线程数设定为CPU核心数或者稍微多一点就好。
- 混合型任务:
既有IO操作又有计算任务。这种情况就要综合考虑,根据具体情况来调配线程数
IO密集型线程池线程数预估
计算公式:线程数 = 核心数 / (1 - 阻塞系数) 为了更好地利用CPU资源,我们可以用这个公式来预估线程数:
线程数 = 核心数 / (1 - 阻塞系数)
假设你的系统有4个CPU核心,IO操作的阻塞系数是0.8。那么线程数应该是多少呢?
线程数 = 4 / (1 - 0.8) = 4 / 0.2 = 20
也就是说,开20个线程差不多就能让CPU一直有事做,效率最高
但是一般情况下我们在设计阶段很难对我们的IO阻塞系统进行预估的,所以这里大家一般情况下都是粗估为CPU核数的2倍+1~2都可以,计算如下:
线程数=4*2+2=10
CPU密集型线程池线程数预估
对于CPU密集型任务,一个比较简单的公式是: 计算公式:线程数 = 核心数 + 1
线程数 = 核心数 + 1
因为这种任务主要耗费CPU资源,通常开多一个线程可以提高点效率,但也不要开太多
假设你的系统有4个CPU核心,那么线程数应该是多少呢?
线程数 = 4 + 1 = 5
也就是说,开5个线程就能比较好地利用CPU资源
混合型线程池线程数预估
混合型任务既有IO操作又有计算任务。这种情况就需要综合考虑两种任务的比例情况,来决定线程数
混合型线程池线程数预估, 参考下面的的公式:
最佳线程数 = ((线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间 ) * CPU 核数
真的免费,如果你近期准备面试跳槽,建议在cxykk.com在线刷题,涵盖 1万+ 道 Java 面试题,几乎覆盖了所有主流技术面试题、简历模板、算法刷题
3.2、线程数的压测验证(测试阶段)
按照上面的线程池理论预估,我们开发好了我们的程序,现在得看看这个数在实际中表现如何。这里就需要用到压测工具。压测就是模拟实际的使用情况,看看我们的系统能不能顶住。
既然压测,那就肯定少不了压测工具,常见的压测工具有:
-
JMeter:开源、功能强大、上手容易,是做压测的神器。
-
Apache Bench (ab):轻量级工具,特别适合对单一URL进行高并发测试。
-
Gatling:相对新一点,但非常强大,特别适合高并发场景。
如何做验证呢?
压测方法:
-
设置测试场景:根据实际使用情况设置压测场景,比如用户登录、下单等。
-
逐步增加并发:从低并发开始,逐步增加,观察系统表现。
-
监控系统指标:记录CPU、内存、响应时间、吞吐量等关键指标。
值得注意的是,在互联网公司中,压测一般是交给专业的测试小伙伴来做的,我们只需要根据他们给出的数据,进行对我们的线程池进行调整,多次验证
关键指标 :
压测过程中,我们需要重点观察几个指标,来判断线程数设置是否合理
-
CPU使用率:看看CPU是否能充分利用,但也不能过载。
-
响应时间:确保系统响应时间在可接受范围内。
-
吞吐量:系统能处理的请求数量,越高越好。
-
错误率:看看是否有大量错误请求。 通过压测,我们可以发现如果线程数设置得太少,CPU使用率低,响应时间长,吞吐量低;
如果线程数设置得太多,CPU过载,频繁上下文切换导致响应时间变长,错误率增加。
3.3、线程数的线上调整(生产阶段)
测的场景,是有限的。而线上的业务, 是复杂的,多样的。
由于系统运行过程中存在的不确定性,很难一劳永逸地规划一个合理的线程数。
所以,需要进行生产阶段线程数的两个目标:
第一维度:可监控预警
第二维度:可在线调整
真的免费,如果你近期准备面试跳槽,建议在cxykk.com在线刷题,涵盖 1万+ 道 Java 面试题,几乎覆盖了所有主流技术面试题、简历模板、算法刷题
四、实战:结合Nacos实现动态化线程池架构
优秀的动态化线程池轮子,主要有:
-
Hippo4J
-
dynamic-tp
如果线上使用,可以使用这些轮子项目。
但是小北还是想要带大家一起从0到1一起去实战下如何通过nacos配置中心动态调整线程池配置 Nacos 实现动态化线程池的参数在线调整,架构如下:
4.1、Nacos线程池配置
1、使用Nacos作为配置中心,将线程池的核心参数配置在Nacos中
threadpool.coreSize=10
threadpool.maxSize=20
threadpool.queueCapacity=50
4.2、线程池配置和Nacos配置变更监听
在应用中实现监听器,监听Nacos配置的变化
@NacosConfigurationProperties(dataId = "threadpool-config", autoRefreshed = true)
public class ThreadPoolConfig {
private int coreSize;
private int maxSize;
private int queueCapacity;
}
4.3、线程池配置的动态刷新
在监听器中动态更新线程池参数
@Autowired
private ThreadPoolExecutor threadPoolExecutor;
@NacosConfigListener(dataId = "threadpool-config")
public void onChange(String configInfo) {
Map<String, Object> originMap = null; // 将content转换为LinkedHashMap类型
try {
originMap = MAPPER.readValue(configInfo, LinkedHashMap.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (null == originMap) return;
Map<Object, Object> result = Maps.newHashMap();
flatMap(result, originMap, MAIN_PROPERTIES_PREFIX);
bindDtpProperties(result, dtpProperties);
DtpExecutor.doRefresh(threadPoolExecutor, dtpProperties);
}
4。4、执行线程池刷新操作
/**
* 真正执行刷新操作的地方
*/
public static void doRefresh(ThreadPoolExecutor executor, DtpProperties props) {
// 更新线程池中的【corePoolSize】和【maximumPoolSize】,以props的数量为主
if (props.getMaximumPoolSize() < executor.getMaximumPoolSize()) {...}
if (!Objects.equals(executor.getMaximumPoolSize(), props.getMaximumPoolSize())) {
executor.setMaximumPoolSize(props.getMaximumPoolSize());
}
if (!Objects.equals(executor.getCorePoolSize(), props.getCorePoolSize())) {
executor.setCorePoolSize(props.getCorePoolSize());
}
executor.setCorePoolSize(props.getCorePoolSize());
// 更新阻塞队列中的【capacity】
val blockingQueue : BlockingQueue<Runnable> = executor.getQueue();
if (!(blockingQueue instanceof ResizableLinkedBlockingQueue)) {
log.warn("DynamicTp refresh, the blockingqueue capacity cannot be reset, poolName: {}, queueType {}",
props.getThreadPoolName(), blockingQueue.getClass().getSimpleName());
return;
}
int capacity = blockingQueue.size() + blockingQueue.remainingCapacity();
if (!Objects.equals(capacity, props.getQueueCapacity())) {
((ResizableLinkedBlockingQueue<Runnable>) blockingQueue).setCapacity(props.getQueueCapacity());
}
}
4.5、LinkedBlockingQueue 实现resize
LinkedBlockingQueue 不支持 resize, 需要重新定制。自定义可以扩容的 LinkedBlockingQueue ,结构如下:
这里采用的是读写锁,对capacity 的设置,进行线程安全 保护:
读写锁的使用如下:
通过对capacity的安全修改,以达到动态扩展目的。
本文总结
看到这里的小伙伴,相信对于如何确定线程池的最佳线程数(核心线程数、最大线程数)心里有一定思路了。 总结下来就三步:理论预估、压测验证、生产监控动态调整**** 小北私藏精品 热门推荐 小北联合公司合伙人,一线大厂在职架构师耗时9个月联合打造了**《** 2024年Java高级架构师课程》本课程对标外面3万左右的架构培训课程,分10个阶段,目前已经更新了181G 视频,已经更新**1000+**个小时视频,一次购买,持续更新,无需2次付费
近期技术热文 架构师必知的绝活-JVM调优
面试官最爱问:CPU100%该如何处理?
架构师必知的11种API性能优化方法
面试官问:百万QPS秒杀系统该如何设计
美团面试:10wqps高并发,如何防止重复下单?
你见过最烂的代码长什么样子?你中招了吗?
第3版:互联网大厂面试题 包括 Java 集合、JVM、多线程、并发编程、设计模式、算法调优、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat、Python、HTML、CSS、Vue、React、JavaScript、Android 大数据、阿里巴巴等大厂面试题等、等技术栈! **阅读原文:**高清 7701页大厂面试题 PDF