Java并发(十七)Java线程池的最佳实践(实际业务场景)

计算线程数量

了解完线程池的实现原理和框架,我们就可以动手实践优化线程池的设置了。
我们知道,环境具有多变性,设置一个绝对精准的线程数其实是不大可能的,但我们可以通过一些实际操作因素来计算出一个合理的线程数,避免由于线程池设置不合理而导致的性能问题。下面我们就来看看具体的计算方法。
一般多线程执行的任务类型可以分为 CPU 密集型和 I/O 密集型,根据不同的任务类型,我们计算线程数的方法也不一样。
**CPU 密集型任务:**这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
下面我们用一个例子来验证下这个方法的可行性,通过观察 CPU 密集型任务在不同线程数下的性能情况就可以得出结果,你可以点击Github下载到本地运行测试:

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;
     }
     /**
     * 計算素数
     * @param number
     * @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));
     }
}

测试代码在 4 核 intel i5 CPU 机器上的运行时间变化如下:
image.png
综上可知:当线程数量太小,同一时间大量请求将被阻塞在线程队列中排队等待执行线程,此时 CPU 没有得到充分利用;当线程数量太大,被创建的执行线程同时在争取 CPU 资源,又会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。通过测试可知,4~6 个线程数是最合适的。
**I/O 密集型任务:**这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
这里我们还是通过一个例子来验证下这个公式是否可以标准化:

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操作
     * @param number
     * @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) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         }
         long end = System.currentTimeMillis();
         long wholeTime = end - initStartTime;
         long runTime = end - start;
         wholeTimeList.add(wholeTime);
         runTimeList.add(runTime);
         System.out.println("单个线程花费时间:" + (end - start));
     }
}

备注:由于测试代码读取 2MB 大小的文件,涉及到大内存,所以在运行之前,我们需要调整 JVM 的堆内存空间:-Xms4g -Xmx4g,避免发生频繁的 FullGC,影响测试结果。
image.png
通过测试结果,我们可以看到每个线程所花费的时间。当线程数量在 8 时,线程平均执行时间是最佳的,这个线程数量和我们的计算公式所得的结果就差不多。
看完以上两种情况下的线程计算方法,你可能还想说,在平常的应用场景中,我们常常遇不到这两种极端情况,那么碰上一些常规的业务操作,比如,通过一个线程池实现向用户定时推送消息的业务,我们又该如何设置线程池的数量呢?
此时我们可以参考以下公式来计算线程数:
线程数=N(CPU核数)(1+WT(线程等待时间)/ST(线程时间运行时间))
我们可以通过 JDK 自带的工具 VisualVM 来查看 WT/ST 比例,以下例子是基于运行纯 CPU 运算的例子,我们可以看到:
WT(线程等待时间)= 36788ms [线程运行总时间] - 36788ms[ST(线程时间运行时间)]= 0
线程数=N(CPU核数)
(1+ 0 [WT(线程等待时间)]/36788ms[ST(线程时间运行时间)])= N(CPU核数)
这跟我们之前通过 CPU 密集型的计算公式 N+1 所得出的结果差不多。
image.png
综合来看,我们可以根据自己的业务场景,从“N+1”和“2N”两个公式中选出一个适合的,计算出一个大概的线程数量,之后通过实际压测,逐渐往“增大线程数量”和“减小线程数量”这两个方向调整,然后观察整体的处理时间变化,最终确定一个具体的线程数量。

参考:https://freegeektime.com/100028001/104094/

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值