科学的线程池数量获取工具【Java】-附可用代码

本文探讨了如何科学地计算Java线程池的大小,基于CPU使用率和线程的等待时间与计算时间比率。对于IO密集型任务,推荐使用两倍CPU核心数的线程数;而对于CPU密集型任务,通常设置为CPU核心数加一。作者提供了线程核心数获取的工具,并引用了《Java虚拟机并发编程》等书籍中的理论。文章还介绍了CPU核心与Java线程的关系,强调线程数应考虑内存限制和实际应用中的阻塞情况。
摘要由CSDN通过智能技术生成

前言

最近有需求可能会使用到线程池,本来是使用本的一个简单的判断逻辑,但是为了自己代码可靠性更高,我重新查询了线程池的科学设置方法。
没耐心可直接去三复制代码

一、科学的线程数计算

最早去了解相关的知识,看到类似以下的公式就头晕,就没有继续深究
在这里插入图片描述
之后我的线程池数量判断就是如下

int i = Runtime.getRuntime().availableProcessors();
ExecutorService executorService = Executors.newFixedThreadPool(i <= 5 ? 10 : 2*i);

这个判断依据来源于《深入理解Java虚拟机中提到的》,Java线程是基于CPU线程实现的,又了解到CPU线程是由于Intel的技术,可以一个核心使用2个线程,所以我根据业务使用了这个公式

i <= 5 ? 10 : 2*i

之后

从网上了解到,有两本书有此相关的内容
《Programming Concurrency on the JVM Mastering》即《Java 虚拟机并发编程》 和 《Java Concurrency in Practice》即 《Java 并发编程实践》

《Java 并发编程实践》 p171
《Java 并发编程实践》 p171

在这里插入图片描述
《Programming Concurrency on the JVM Mastering》 p17

那么,基于以上,最常用的、保持处理器达到期望的使用率,最优的线程池的大小等于

  • Nthreads=NcpuUcpu(1+W/C)

然后关于网上流传的

  • IO密集型使用2倍CPU核心数的线程数
  • CPU密集型使用N+1的线程数

以上说法根据公式验证如下

  • IO密集型:一般情况下,如果存在IO,那么肯定w/c>1(阻塞耗时一般都是计算耗时的很多倍),但是需要考虑系统内存有限(每开启一个线程都需要内存空间),
    这里需要上服务器测试具体多少个线程数适合(CPU占比、线程数、总耗时、内存消耗)。如果不想去测试,保守点取1即,Nthreads=Ncpu(1+1)=2Ncpu。这样设置一般都OK。

  • CPU密集型:假设没有等待w=0,则W/C=0. Nthreads=Ncpu。不过在工程上,线程的数量一般会设置为“CPU 核数 +1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率

二、CPU和Java中的核心和线程

前面提到,我在《深入理解Java虚拟机中提到的》中看到关于Java线程实现基于机器的线程数,然后机器的线程一般一个核心最多实现2个,但是我CPU是i5-8500,6核心理论上最多12线程,但实际他有
在这里插入图片描述
266个进程,3898个线程,这部分就是CPU分时调度的的问题了这就不说了,

简单来说就是

原来操作系统是采用的是时间片轮转的抢占式调度方式,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离,
由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”.

而对应的,Java就是

Java程序可以算是一个进程,Java的线程在jvm里分配,jvm模拟了虚拟的电脑运行环境

而实际上Java的线程和操作系统的CPU不是一个概念,Java的线程数的多少是根据机器内存大小决定的

这取决于你使用的CPU,操作系统,其他进程正在做的事情,你使用的Java的版本,还有其他的因素。我曾经见过一台Windows服务器在宕机之前有超过6500个线程。当然,大多数线程什么事情也没有做。一旦一台机器上有差不多6500个线程(Java里面),机器就会开始出问题,并变得不稳定。
以我的经验来看,JVM容纳的线程与计算机本身性能是正相关的。
当然了,你要有足够的本机内存,并且给Java分配了足够的内存,让每个线程都可以拥有栈(虚拟机栈),可以做任何想做的事情。任何一台拥有现代CPU(AMD或者是Intel最近的几代)和1-2G内存(取决于操作系统)的机器很容易就可以支持有上千个线程的Java虚拟机。

绝对理论上的最大线程数是进程的用户地址空间除以线程栈的大小(现实中,如果内存全部给线程栈使用,就不会有能运行的程序了)。因此,以32位Windows系统为例,每一个进程的用户地址空间是2G,假如每个线程栈的大小是128K,最多会有16384(=210241024 / 128)个线程。实际在XP系统上,我发现大约能启动13000个线程。
如果你需要一个更精确的答案,最好是自己做压测。

总之一个简单的例子,循环创建1000条线程1秒不到就结束了,如果循环创建的1000条线程每条阻塞500ms,那就能看出来你的机器每秒能并发执行的线程数,前面只不过是执行的太快,导致的1条线程1秒能跑许多条线程,给人的感觉就是1秒跑了1000条

如果用这个500ms的阻塞代表日常的IO/CPU阻塞,就可以看出,一开始给的公式是有道理的,CPU有没有压力所能处理的线程数是不一样的,接下来就直接给大家看看能直接使用的线程数获取工具。

三、线程核心数获取

此方法基于《Programming Concurrency on the JVM Mastering》 第17页的公式

以上大部分解释都会作为注释出现在下面,光看代码其实也是可以的

/**
 * 定义线程核心数获取
 * 最常见的公式
 * Ncpu=CPU的数量
 * Ucpu=目标CPU使用率
 * W/C=等待时间与计算时间的比率
 *
 * 为保持处理器达到期望的使用率,最优的线程池的大小等于
 * Nthreads=Ncpu*Ucpu*(1+W/C)
 *
 * 基于以上
 *
 * IO密集型:一般情况下,如果存在IO,那么肯定w/c>1(阻塞耗时一般都是计算耗时的很多倍),但是需要考虑系统内存有限(每开启一个线程都需要内存空间),
 * 这里需要上服务器测试具体多少个线程数适合(CPU占比、线程数、总耗时、内存消耗)。如果不想去测试,保守点取1即,Nthreads=Ncpu*(1+1)=2Ncpu。这样设置一般都OK。
 *
 * CPU密集型:假设没有等待w=0,则W/C=0. Nthreads=Ncpu。
 *
 * 在《Programming Concurrency on the JVM Mastering》即《Java 虚拟机并发编程》中总结为一个公式
 * 线程数 = CPU可用核心数/(1 - 阻塞系数), 其中阻塞系数介于 0 和 1 之间。
 * @author Genmer
 *
 */
public final class ThreadPoolSizeUtil {
    
    private ThreadPoolSizeUtil() {
        
    }
    /**
     * 每个任务有 90%(大部分) 的时间会阻塞,并且只在其生命周期的 10%(小部分) 内工作。即I/O密集池
     * 这里不确定工作机器的内存情况,故保守取2倍线程数
     * @return io intesive Thread pool size
     */
    public static int ioIntesivePoolSize() {
        
        double blockingCoefficient = 0.5;
        return poolSize(blockingCoefficient);
    }

    /**
     * 每个任务有 90%(大部分) 的时间会阻塞,并且只在其生命周期的 10%(小部分) 内工作。即I/O密集池
     * 对于 CPU 密集型的计算场景,理论上“线程的数量 =CPU 核数”就是最合适的。
     * 不过在工程上,线程的数量一般会设置为“CPU 核数 +1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率
     * @return cpu intesive Thread pool size
     */
    public static int cpuIntesivePoolSize() {

        double blockingCoefficient = 0;
        return poolSize(blockingCoefficient) + 1;
    }


    /**
     * 
     * 线程数 = 可用内核数 / (1 - 阻塞 * 系数),其中阻塞系数介于 0 和 1 之间。
     *  CPU密集型任务的阻塞系数为 0,而  IO 密集型任务具有值接近 1。
     *  @param blockingCoefficient the coefficient
     *  @return Thread pool size
     */
    public static int poolSize(double blockingCoefficient) {
        // 获取JVM可使用的逻辑核心数
        int numberOfCores = Runtime.getRuntime().availableProcessors();
        int poolSize = (int) (numberOfCores / (1 - blockingCoefficient));
        return poolSize;
    }
}

四、参考资料

  1. 根据CPU核心数确定线程池并发线程数
  2. 关于CPU核心,线程,进程,并发,并行,及java线程之间的关系
  3. IO密集型的线程池大小设置
  4. JVM最多支持多少个线程?
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值