前言
最近有需求可能会使用到线程池,本来是使用本的一个简单的判断逻辑,但是为了自己代码可靠性更高,我重新查询了线程池的科学设置方法。
没耐心可直接去三复制代码
一、科学的线程数计算
最早去了解相关的知识,看到类似以下的公式就头晕,就没有继续深究
之后我的线程池数量判断就是如下
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
《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;
}
}