乐趣区

关于java:别再纠结线程池大小线程数量了没有固定公式的

可能很多人都看到过一个线程数设置的实践:

  • CPU 密集型的程序 – 外围数 + 1
  • I/O 密集型的程序 – 外围数 * 2

不会吧,不会吧,真的有人依照这个实践布局线程数?

线程数和 CPU 的小测试

抛开一些操作系统,计算机原理不谈,说一个根本的实践(不必纠结是否谨严,只为好了解):

一个 CPU 外围,单位工夫内只能执行一个线程的指令

那么实践上,我一个线程只须要不停的执行指令,就能够跑满一个外围的利用率。

来写个死循环空跑的例子验证一下:

测试环境:AMD Ryzen 5 3600, 6 – Core, 12 – Threads

public class CPUUtilizationTest {public static void main(String[] args) {
        // 死循环,什么都不做
        while (true){}}
}

运行这个例子后,来看看当初 CPU 的利用率:

从图上能够看到,我的 3 号外围利用率曾经被跑满了

那基于下面的实践,我多开几个线程试试呢?

public class CPUUtilizationTest {public static void main(String[] args) {for (int j = 0; j < 6; j++) {new Thread(new Runnable() {
                @Override
                public void run() {while (true){}}
            }).start();}
    }
}

此时再看 CPU 利用率,1/2/5/7/9/11 几个外围的利用率曾经被跑满:

那如果开 12 个线程呢,是不是会把所有外围的利用率都跑满?答案肯定是会的:

如果此时我把下面例子的线程数持续减少到 24 个线程,会呈现什么后果呢?

从上图能够看到,CPU 利用率和上一步一样,还是所有外围 100%,不过此时负载曾经从 11.x 减少到了 22.x(load average 解释参考 https://scoutapm.com/blog/understanding-load-averages),阐明此时 CPU 更忙碌,线程的工作无奈及时执行。

古代 CPU 根本都是多外围的,比方我这里测试用的 AMD 3600,6 外围 12 线程(超线程),咱们能够简略的认为它就是 12 外围 CPU。那么我这个 CPU 就能够同时做 12 件事,互不打搅。

如果要执行的线程大于外围数,那么就须要通过操作系统的调度了。操作系统给每个线程调配 CPU 工夫片资源,而后不停的切换,从而实现“并行”执行的成果。

然而这样真的更快吗?从下面的例子能够看出,一个线程 就能够把 一个外围 的利用率跑满。如果每个线程都很“王道”,不停的执行指令,不给 CPU 闲暇的工夫,并且同时执行的线程数大于 CPU 的外围数,就会导致操作系统 更频繁的执行切换线程执行,以确保每个线程都能够失去执行。

不过切换是有代价的,每次切换会随同着寄存器数据更新,内存页表更新等操作。尽管一次切换的代价和 I / O 操作比起来微不足道,但如果线程过多,线程切换的过于频繁,甚至在单位工夫内切换的耗时曾经大于程序执行的工夫,就会导致 CPU 资源过多的节约在上下文切换上,而不是在执行程序,得失相当。

下面死循环空跑的例子,有点过于极其了,失常状况下不太可能有这种程序。

大多程序在运行时都会有一些 I/ O 操作,可能是读写文件,网络收发报文等,这些 I/O 操作在进行时时须要期待反馈的。比方网络读写时,须要期待报文发送或者接管到,在这个期待过程中,线程是期待状态,CPU 没有工作。此时操作系统就会调度 CPU 去执行其余线程的指令,这样就完满利用了 CPU 这段闲暇期,进步了 CPU 的利用率。

下面的例子中,程序不停的循环什么都不做,CPU 要不停的执行指令,简直没有啥闲暇的工夫。如果插入一段 I / O 操作呢,I/O 操作期间 CPU 是闲暇状态,CPU 的利用率会怎么样呢?先看看单线程下的后果:

public class CPUUtilizationTest {public static void main(String[] args) throws InterruptedException {for (int n = 0; n < 1; n++) {new Thread(new Runnable() {
                @Override
                public void run() {while (true){
                        // 每次空循环 1 亿 次后,sleep 50ms,模仿 I/ O 期待、切换
                        for (int i = 0; i < 100_000_000l; i++) { }
                        try {Thread.sleep(50);
                        }
                        catch (InterruptedException e) {e.printStackTrace();
                        }
                    }
                }
            }).start();}
    }
}

哇,惟一有利用率的 9 号外围,利用率也才 50%,和后面没有 sleep 的 100% 相比,曾经低了一半了。当初把线程数调整到 12 个看看:

单个外围的利用率 60 左右,和方才的单线程后果差距不大,还没有把 CPU 利用率跑满,当初将线程数减少到 18:

此时单核心利用率,曾经靠近 100% 了。由此可见,当线程中有 I/O 等操作不占用 CPU 资源时,操作系统能够调度 CPU 能够同时执行更多的线程。

当初将 I / O 事件的频率调高看看呢,把循环次数减到一半,50_000_000,同样是 18 个线程:

此时每个外围的利用率,大略只有 70% 左右了。

线程数和 CPU 利用率的小总结

下面的例子,只是辅助,为了更好的了解线程数 / 程序行为 /CPU 状态的关系,来简略总结一下:

  1. 一个极其的线程(不停执行“计算”型操作时),就能够把单个外围的利用率跑满,多外围 CPU 最多只能同时执行等于外围数的“极其”线程数
  2. 如果每个线程都这么“极其”,且同时执行的线程数超过外围数,会导致不必要的切换,造成负载过高,只会让执行更慢
  3. I/O 等暂停类操作时,CPU 处于闲暇状态,操作系统调度 CPU 执行其余线程,能够进步 CPU 利用率,同时执行更多的线程
  4. I/O 事件的频率频率越高,或者期待 / 暂停工夫越长,CPU 的闲暇工夫也就更长,利用率越低,操作系统能够调度 CPU 执行更多的线程

    线程数布局的公式

    后面的铺垫,都是为了帮忙了解,当初来看看书本上的定义。《Java 并发编程实战》介绍了一个线程数计算的公式:

$$Ncpu=CPU 外围数 $$

$$Ucpu= 指标 CPU 利用率,0<=Ucpu<=1$$

$$\frac{W}{C}= 等待时间和计算工夫的比例 $$

如果心愿程序跑到 CPU 的指标利用率,须要的线程数公式为:

$$Nthreads=Ncpu*Ucpu*(1+\frac{W}{C})$$

公式很清晰,当初来带入下面的例子试试看:

如果我冀望指标利用率为 90%(多核 90),那么须要的线程数为:

外围数 12 利用率 0.9 (1 + 50(sleep 工夫)/50(循环 50_000_000 耗时)) ≈ 22

当初把线程数调到 22,看看后果:

当初 CPU 利用率大略 80+,和预期比拟靠近了,因为线程数过多,还有些上下文切换的开销,再加上测试用例不够谨严,所以理论利用率低一些也失常。

把公式变个形,还能够通过线程数来计算 CPU 利用率:

$$Ucpu=\frac{Nthreads}{Ncpu*(1+\frac{W}{C})}$$

线程数 22 / (外围数 12 * (1 + 50(sleep 工夫)/50(循环 50_000_000 耗时))) ≈ 0.9

尽管公式很好,但在实在的程序中,个别很难取得精确的等待时间和计算工夫,因为程序很简单,不只是“计算”。一段代码中会有很多的内存读写,计算,I/O 等复合操作,准确的获取这两个指标很难,所以光靠公式计算线程数过于理想化。

实在程序中的线程数

那么在理论的程序中,或者说一些 Java 的业务零碎中,线程数(线程池大小)布局多少适合呢?

先说论断:没有固定答案,先设定预期,比方我冀望的 CPU 利用率在多少,负载在多少,GC 频率多少之类的指标后,再通过测试一直的调整到一个正当的线程数

比方一个一般的,SpringBoot 为根底的业务零碎,默认 Tomcat 容器 +HikariCP 连接池 +G1 回收器,如果此时我的项目中也须要一个业务场景的多线程(或者线程池)来异步 / 并行执行业务流程。

此时我依照下面的公式来布局线程数的话,误差肯定会很大。因为此时这台主机上,曾经有很多运行中的线程了,Tomcat 有本人的线程池,HikariCP 也有本人的后盾线程,JVM 也有一些编译的线程,连 G1 都有本人的后盾线程。这些线程也是运行在以后过程、以后主机上的,也会占用 CPU 的资源。

不过对于业务零碎来说,个别这些线程不太会成为瓶颈

所以受环境烦扰下,单靠公式很难精确的布局线程数,肯定要通过测试来验证。

流程个别是这样:

  1. 剖析以后主机上,有没有其余过程烦扰
  2. 剖析以后 JVM 过程上,有没有其余运行中或可能运行的线程
  3. 设定指标

    1. 指标 CPU 利用率 – 我最高能容忍我的 CPU 飙到多少?
    2. 指标 GC 频率 / 暂停工夫 – 多线程执行后,GC 频率会增高,最大能容忍到什么频率,每次暂停工夫多少?
    3. 执行效率 – 比方批处理时,我单位工夫内要开多少线程能力及时处理完毕
    4. ……
  4. 梳理链路关键点,是否有卡脖子的点,因为如果线程数过多,链路上某些节点资源无限可能会导致大量的线程在期待资源(比方三方接口限流,连接池数量无限,中间件压力过大无奈撑持等)
  5. 一直的减少 / 缩小线程数来测试,按最高的要求去测试,最终取得一个“满足要求”的线程数 **

而且而且而且!不同场景下的线程数理念也有所不同:

  1. Tomcat 中的 maxThreads,在 Blocking I/ O 和 No-Blocking I/ O 下就不一样
  2. Dubbo 默认还是单连贯呢,也有 I / O 线程(池)和业务线程(池)的辨别,I/ O 线程个别不是瓶颈,所以不用太多,但业务线程很容易称为瓶颈
  3. Redis 6.0 当前也是多线程了,不过它只是 I /O 多线程,“业务”解决还是单线程

所以,不要纠结设置多少线程了。没有标准答案,肯定要联合场景,带着指标,通过测试去找到一个最合适的线程数。

可能还有同学可能会有疑难:“咱们零碎也没啥压力,不须要那么适合的线程数,只是一个简略的异步场景,不影响零碎其余性能就能够”

很失常,很多的外部业务零碎,并不需要啥性能,稳固好用合乎需要就能够了。那么我的举荐的线程数是:CPU 外围数

附录

Java 获取 CPU 外围数

Runtime.getRuntime().availableProcessors()// 获取逻辑外围数,如 6 外围 12 线程,那么返回的是 12

Linux 获取 CPU 外围数

# 总核数 = 物理 CPU 个数 X 每颗物理 CPU 的核数 
# 总逻辑 CPU 数 = 物理 CPU 个数 X 每颗物理 CPU 的核数 X 超线程数

# 查看物理 CPU 个数
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l

# 查看每个物理 CPU 中 core 的个数(即核数)
cat /proc/cpuinfo| grep "cpu cores"| uniq

# 查看逻辑 CPU 的个数
cat /proc/cpuinfo| grep "processor"| wc -l

原创不易,转载请分割作者。如果我的文章对您有帮忙,请点赞 / 珍藏 / 关注激励反对一下吧❤❤❤❤❤❤

退出移动版