vlambda博客
学习文章列表

别再纠结,线程池大小 + 线程数量了,没有固定公式的

大家好,我是磊哥。

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

1、 CPU 密集型的程序 - 核心数 + 1
2、 I/O 密集型的程序 - 核心数 * 2

不会吧,不会吧,真的有人按照这个理论规划线程数?

线程数和CPU利用率的小测试

抛开一些操作系统,计算机原理不谈,说一个基本的理论(不用纠结是否严谨,只为好理解):一个CPU核心,单位时间内只能执行一个线程的指令 ** 那么理论上,我一个线程只需要不停的执行指令,就可以跑满一个核心的利用率。

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

注 意
 文末有:3625页互联网大厂面试题 

测试环境: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个线程,会出现什么结果呢?

别再纠结,线程池大小 + 线程数量了,没有固定公式的

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利用率的小总结

线程数规划的公式

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

别再纠结,线程池大小 + 线程数量了,没有固定公式的

如果希望程序跑到CPU的目标利用率,需要的线程数公式为:

别再纠结,线程池大小 + 线程数量了,没有固定公式的

公式很清晰,现在来带入上面的例子试试看:

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

别再纠结,线程池大小 + 线程数量了,没有固定公式的

现在把线程数调到22,看看结果:

现在CPU利用率大概80+,和预期比较接近了,由于线程数过多,还有些上下文切换的开销,再加上测试用例不够严谨,所以实际利用率低一些也正常。

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

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

真实程序中的线程数

流程一般是这样:

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

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

来源 | https://sourl.cn/wrJJPs


近期技术热文

1、
2、
3、
4、