前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

1、cpu 时代

第一阶段,单 CPU 时代,单 CPU 在同一时间点,只能执行单一线程。比如,的某一刻 00:00:00 这一秒,只计算1+1=2(假设cpu每秒计算一次)

第二阶段,单CPU多任务阶段,计算机在同一时间点,并行执行多个线程。但这并非真正意义上的同时执行,而是多个任务共享一个CPU,操作系统协调CPU在某个时间点,执行某个线程,因为CPU在线程之间切换比较快,给人的感觉,就好像多个任务在同时运行。比如,电脑开了两个程序qq和qq音乐,假设这两个程序都只有一个线程。人能够感觉到CPU切换的频率是一秒一次,假设当前cpu计算速度是1秒1次,那么我们就能明显感到卡顿,当聊天,点击发送按钮时候,qq音乐就会停止运行。当前cpu计算速度是1秒100次,也就是它能在一秒之内在这两个进程见切换100次,那么我们就感不到卡顿,觉得QQ和QQ音乐是同时在运行。

第三阶段,多 CPU 多任务阶段,真正实现的,在同一时间点运行多个线程。具体到哪个线程在哪个CPU执行,这就跟操作系统和CPU本身的设计有关了。

2、线程数设置的原则

1、系统的资源状况(处理器的数目,内存容量,CPU使用率上限)

2、线程所执行任务的特性(cpu密集型,i/o密集型)

3、设计线程数要尽可能考虑其他所有进程内部线程数设置的情况。

CPU密集型线程:如果是CPU密集型任务,就需要尽量压榨CPU,考虑到这类线程执行任务时消耗主要是处理器资源,我们可以将这类线程数设置为Ncpu,有时候,因为CPU密集型线程也可能由于某些原因被切出,为了避免处理器资源浪费,可以为它添加一个额外的线程Ncpu+1

I/O密集型线程:考虑到I/O操作可能导致上下文切换,为这样的线程设置过多的线程数会导致额外的系统开销,在I/O密集型线程在等待I/O操作返回结果的时候不占用处理器资源。因此我们可以为每个处理器安排一个额外的线程来提高处理器资源的利用率。所以设置为Ncpu*2

public class NumMain {
    public static void main(String[] args) {
        System.out.println(Runtime.getRuntime().availableProcessors());
    }
}

2.1、验证cup密集型 Ncpu+1 是否可行

2.1.1、代码-摘自网络

摘抄自网络

import java.util.List;

public class CPUTypeTest implements Runnable {


    // 整体执行时间,包括在队列中等待的时间
    List<Long> wholeTimeList;
    // 真正执行时间
    List<Long> runTimeList;
    private long initStartTime = 0;

    /**
     * 构造函数
     * @param runTimeList
     * @param wholeTimeList
     */
    public CPUTypeTest(List<Long> runTimeList, List<Long> wholeTimeList) {
        initStartTime = System.currentTimeMillis();
        this.runTimeList = runTimeList;
        this.wholeTimeList = wholeTimeList;
    }

    /**
     * 判断素数
     * @param number
     * @return
     */
    public boolean isPrime(final int number) {
        if (number <= 1)
            return false;
        for (int i = 2; i <= Math.sqrt(number); i++) {
            if (number % i == 0)
                return false;
        }
        return true;
    }

    /**
     * 計算素数
     * @return
     */
    public int countPrimes(final int lower, final int upper) {
        int total = 0;
        for (int i = lower; i <= upper; i++) {
            if (isPrime(i))
                total++;
        }
        return total;
    }

    public void run() {
        long start = System.currentTimeMillis();
        countPrimes(1, 1000000);
        long end = System.currentTimeMillis();

        long wholeTime = end - initStartTime;
        long runTime = end - start;
        wholeTimeList.add(wholeTime);
        runTimeList.add(runTime);
        System.out.println(" 单个线程花费时间:" + (end - start));
    }
}

2.1.2、分析

测试代码在 4 核 intel i5 CPU 机器上的运行时间变化如下:

image-20210730144823839

2.1.3、总结

1、当线程数量太小,同一时间大量请求将被阻塞在线程队列中排队等待执行线程,此时 CPU 没有得到充分利用;

2、当线程数量太大,被创建的执行线程同时在争取 CPU 资源,又会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

3、通过测试可知,4~6 个线程数是最合适的。

2.2、io密集型任务

这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N

2.2.1、代码-摘自网络

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Vector;

/**
 * @author zhangyujin
 * @date 2021/7/30  2:22 下午.
 * @description
 */
public class IOTypeTest implements Runnable {


    // 整体执行时间,包括在队列中等待的时间
    Vector<Long> wholeTimeList;
    // 真正执行时间
    Vector<Long> runTimeList;


    private long initStartTime = 0;


    /**
     * 构造函数
     *
     * @param runTimeList
     * @param wholeTimeList
     */
    public IOTypeTest(Vector<Long> runTimeList, Vector<Long> wholeTimeList) {
        initStartTime = System.currentTimeMillis();
        this.runTimeList = runTimeList;
        this.wholeTimeList = wholeTimeList;
    }


    /**
     * IO 操作
     *
     * @return
     * @throws IOException
     */
    public void readAndWrite() throws IOException {
        File sourceFile = new File("D:/test.txt");
        // 创建输入流
        BufferedReader input = new BufferedReader(new FileReader(sourceFile));
        // 读取源文件, 写入到新的文件
        String line = null;
        while ((line = input.readLine()) != null) {
        //System.out.println(line);
        }
        // 关闭输入输出流
        input.close();
    }


    public void run() {
        long start = System.currentTimeMillis();
        try {
            readAndWrite();
        } catch (IOException e) {

        }
        long end = System.currentTimeMillis();
        long wholeTime = end - initStartTime;
        long runTime = end - start;
        wholeTimeList.add(wholeTime);
        runTimeList.add(runTime);
        System.out.println(" 单个线程花费时间:" + (end - start));
    }
}

2.2.2、分析

image-20210730145243354

2.2.3、总结

1、当线程数量在 8 时,线程平均执行时间是最佳的,这个线程数量和我们的计算公式所得的结果就差不多。

2.3、非极端情况下怎么设置

看完以上两种情况下的线程计算方法,你可能还想说,在平常的应用场景中,我们常常遇不到这两种极端情况,那么碰上一些常规的业务操作

比如,通过一个线程池实现向用户定时推送消息的业务,我们又该如何设置线程池的数量呢?此时我们可以参考以下公式来计算线程数:

根据自己的业务场景,从“N+1”和“2N”两个公式中选出一个适合的,计算出一个大概的线程数量,之后通过实际压测,逐渐往“增大线程数量”和“减小线程数量”这两个方向调整,然后观察整体的处理时间变化,最终确定一个具体的线程数量

一般说来,大家认为线程池的大小经验值应该这样设置:(其中N为CPU的个数)

⬤ 如果是CPU密集型应用,则线程池大小设置为N+1

⬤ 如果是IO密集型应用,则线程池大小设置为2N+1

如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需自行测试验证。但是,IO优化中,这样的估算公式可能更适合:

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

因为很显然,线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

下面举个例子:

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:

实际业务场景参数:

  • tasks: 每秒的任务数,假设是500~1000

  • taskcost: 每个任务花费的时间,假设为0.1s

  • responsetime: 系统允许容忍的最大响应时间,假设为1s

    结合利特尔法则做几个计算:

  1. corePoolSize = 每秒需要多少个线程处理?
  • threadcount = tasks/(1/taskcost) =tasks*taskcost =(500~1000)*0.1 = 50~100 个线程。corePoolSize设置应该大于50
  • 根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可
  1. queueCapacity = (coreSizePool/taskcost)responsetime = 80/0.1 * 1 = 800 (0.1s 线程池可处理80个任务,1s 线程池可处理800个任务)也就是说队列里的线程可以等待1s,超过了的需要新开线程来执行
  • 切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
  1. maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)(最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数)
  2. rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理。
  3. keepAliveTime和allowCoreThreadTimeout采用默认通常能满足。

ContactAuthor