51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

面试官必问:线程池最佳核心线程数该如何确定?

一、为什么要学习线程池最佳线程数的确定

系统线程池的最佳核心线程、最大线程数的确定,是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

赞(8)
未经允许不得转载:工具盒子 » 面试官必问:线程池最佳核心线程数该如何确定?