深入研究池化技术——线程池

11 篇文章 3 订阅
5 篇文章 0 订阅

线程池

线程池顾名思义就是一个放线程的池子,使用线程池的好处有很多:

  • 重用已存在的线程
  • 控制并发
  • 功能强大

从某种意义上来讲,线程池是一种特殊的对象池,因此你也可以使用对象池来自己实现一个线程池,当然一般情况下我们是不需要自己实现线程池的,使用JDK自带的线程池就可以了。

话不多说,我们直接开干!
首先我们用ThreadPoolExecutor创建一个线程池玩一玩:
在这里插入图片描述
可以看到,它的构造方法里最多可以传入七个构造参数,我们就直接玩这个构造参数最多的吧。

	ThreadPoolExecutor executor = new ThreadPoolExecutor(
                10,
                10,
                10L,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );

这样一个线程池就创建好了,来看一下每个构造参数什么意思:

  • corePoolSize:核心线程数
    线程池里的线程有两种,一种叫核心线程,另一种叫做非核心线程。
  • maximumPoolSize:最大线程数
    也就是核心线程加非核心线程的总数。
  • keepAliveTime:线程允许的空闲时间
    如果超过这个时间,线程将被终止。
    默认情况下指的是非核心线程的空闲时间,但是如果设置了executor.allowCoreThreadTimeOut(true);那么表达的是核心线程或非核心线程允许的空闲时间。
  • TimeUnit:keepAliveTime的时间单位
  • workQueue:存储等待执行的任务,传入BlockingQueue
  • threadFactory:用于创建线程的线程工厂
    默认情况下使用的是defaultThreadFactory。此外还有privilegedThreadFactory。
    其中PrivilegedThreadFactory继承了DefaultThreadFactory,可以让运行在这个线程中的任务拥有和这个线程相同的访问控制和ClassLoader。实际项目中使用DefaultThreadFactory的场景更多一些。
  • rejectHandler:拒绝任务的策略
    如果workQueue满了,当前的线程池也没有空闲的线程,当时依然还有任务被提交进来,这个时候就需要使用拒绝策略去处理这个任务。
    • AbortPolicy(默认):抛异常
    • CallerRusPolicy:用调用者所在的线程执行任务
    • DiscardOldestPolicy:丢弃队列中最靠前的任务
    • DiscardPolicy:丢弃当前任务

了解完线程池的构造参数后,我们执行一个任务玩一玩:
在这里插入图片描述
这样就是一个线程池,用起来还是比较简单的,我们运行一下看看:
在这里插入图片描述
关于线程池的核心API有两中,一种是操作类API,另一种是监控类API。

操作类API

  • execute():提交任务,交给线程池执行
  • submit():提交任务,能够返回执行结果
  • shutdown():关闭线程池,等待任务都执行完
  • shutdownNow():关闭线程池,不等待任务执行完(很少使用)

监控类API

  • getTaskCount():返回线程池已执行和未执行的任务总数
  • getCompletedTaskCount():已完成的任务数量
  • getPoolSize():线程池当前的线程数量
  • getActiveCount():线程池中正在执行任务的线程数量

线程池状态

线程池还维护了一个状态机,注意,这里的线程池状态不是线程的状态。
在这里插入图片描述
上图中描述了状态的迁移
RUNNING指的是当前线程池可以接受提交的新任务,也可以处理阻塞队列中的任务,RUNNING状态的线程池调用shutdown方法,就可以把线程池的状态转变为SHUTDOWN;SHUTDOWN表示不能提交新的任务,但是可以处理阻塞队列里的任务;RUNNING状态的线程池调用shutdownNow就可以把线程池的状态转变为STOP,STOP表示不能提交新的任务,也不会处理队列中的任务,他会直接中断处理中的任务;TIDYING表示如果所有的任务都终止的话就进入这个状态了。如果线程在SHUTDOWN状态的话,阻塞队列为空并且线程池里执行的任务也为空的话才会变为TIDYING,如果线程池是STOP状态的话,线程池里执行的任务为空就会进入TIDYING;TIDYING状态的线程池调用terminated方法就会进入到TERMINATED状态,TERMINATED状态表示线程池彻底终止。

BlockingQueue

BlockingQueue也叫阻塞队列,它是J.U.C包里的非常常用的接口,说到队列大家应该都很熟悉,就是排队先进先出的意思。
那么阻塞队列是,当队列为空时,获取对象会阻塞当前线程,而队列满了的话,放入对象时会阻塞当前线程。
前面我们用的LinedBlockingQueue是BlockingQueue的一个实现类。
BlockingQueue的主要作用:

  • 实现队列应该具备的基本功能
  • 在多线程环境下,自动管理线程的等待与唤醒

Blocking提供了丰富的API,我做了一个总结:

抛出异常特殊值阻塞超时
插入add(e)offer(e)put(e)offer(e, time, unit)
移除remove()poll()take()poll(time, unit)
检查element()peek()--

我们再来探讨一下LinedBlockingQueue下的几个常用实现:

类名有界特性
ArrayBlockingQueue有界1.内部基于数组实现 2. 初始化时必须指定容量大小(因为没有无参构造器) 3.一旦指定容量大小,就不能修改了
LinedTransferQueue无界1.底层基于链表 2.用于数据交换 3.比其他队列多了transfer()及tryTransfer()方法
DelayQueue1.其中的元素必须实现Delayed接口 2.其中的元素需要排序,一般情况下按照过期时间的优先级排序。3.使用场景:定时关闭连接、缓存对象、超时处理的场景
LinkedBlockingQueue有界/无界1.容量可选,默认无界(Integer.MAX_VALUE) 2.内部基于链表实现
PriorityBlockingQueue无界1.带优先级的阻塞队列 2.允许插入NULL对象 3.元素必须实现Comparable接口,队列的排序规则需要用
SynchronousQueue有界1.不存储元素,内部容量为0 2.一个线程发起插入操作后,会被阻塞,直到另一个线程发起相应的删除操作才会恢复,因此又被称为同步队列 3.主要实现了take()和put()操作,且需配对使用。

线程池调优技巧

要想使线程池处理任务的吞吐量达到合理的范围,想让线程调度相对简单,并且还能尽可能的降低线程池对资源的消耗,那么就必须合理设置corePoolSize、maximumPoolSize、workQueue的容量。

我们想要降低系统资源的消耗,比如CPU的使用率,操作系统的资源消耗,上下文切换的开销,那么可以设置一个比较大的队列容量,和一个比较小的线程池容量。这样多余的任务就会在队列里排队,线程的个数会保持一个比较有界的范围,这样线程的开销就比较小了。

又比如,任务经常发生阻塞,说明队列经常满,这时候可以考虑用executor.setMaximumPoolSize()重新设置线程池容量。

ScheduledThreadPoolExecutor

它是ThreadPoolExecutor的子类,在原有的基础上,支持了延时执行、周期性执行任务。

在这里插入图片描述
它也有多个构造方法,最多可以传入三个参数,我们跟踪进源码看一下:
在这里插入图片描述
第一个是corePoolSize表示核心线程数,第二个threadFactory是线程工厂,第三个handler是拒绝策略。在里面,它的super调用了ThreadPoolExecuter的构造方法。
我们看一下它的核心方法,都以schedule开头,一共有四个。
在这里插入图片描述
当然,因为ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,所以ThreadPoolExecutor的核心API可以继续使用。

我们来玩一下:

 	public static void main(String[] args) {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
                10,
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );

        long start = System.currentTimeMillis();
        //延迟3秒再启动这个定时任务。
        executor.schedule(() -> {
            long end = System.currentTimeMillis();

            System.out.println("执行结束,耗时:"+(end - start));
        }, 3, TimeUnit.SECONDS);
    }

在这里插入图片描述
可以看到,任务三秒后执行了。

总结

总结一下,ScheduledThreadPoolExecutor有四个核心API:

  • schedule(Runnable, long, timeUnit)
  • schedule(Callable, long, TimeUnit)

传入Runnable表示延迟多久执行,传入Callable作用和传入Runnable是一样的,但是可以拿到任务的返回结果。

  • scheduleAtFixedRate
    表示每隔多久执行一次
  • scheduleWithFixedDelay
    每次执行任务之后延迟多久再此执行

ForkJoinPool

ForkJoinPool是JDK 7 开始提供的线程池
它的思想是:把一个大任务拆分成若干个小任务,最终再把每个小任务的结果汇总的框架
那么,拆分任务的过程叫做Fork,汇总任务的过程叫做Join

ForkJoinPool比较适合分而治之、递归计算的CPU密集场景。

它的最大亮点是实现了工作窃取算法。

ForkJoinPool原理

下面我们来分析一下ForkJoinPool原理
在这里插入图片描述
这张图里面,我们把一个大任务拆分成了若干个子任务,图上画了八个,然后我们ForkJoinPool里面弄了两个线程,这些子任务被放到了两个队列里面,每个队列都会有一个线程来消费执行。线程和队列是一一对应的,并且在正常情况下,线程会以LIFO也就是后进先出的方式从自己的队列里面获取任务去执行,如果发现自己的队列任务已经空了,任务已经全部消费完成的话,就会检查其他的线程里面有没有尚未执行的任务,比如图中的ForkJoinWorkThread-1里面的任务执行完了,就会去ForkJoinWorkThread-2里面有没有尚未执行的任务,如果有的话,就会使用FIFO,也就是先进先出的方式,从ForkJoinWorkThread-2的队列里面窃取任务执行。
也就是说,线程在执行自己任务的时候,是以后进先出的方式执行的,也就是从顶端开始消费,而执行别人的任务是先进先出的,从底部消费,这样可以减少争抢。当所有任务都执行完后返回结果。

经过分析,我们不难发现,使用ForkJoinPool可以充分利用线程实现并行计算,同时还可以利用工作窃取提升性能。

我们来玩一下ForkJoinPool
我们来玩一个比较经典的Case,就是实现1-100的求和,我们使用submit:
在这里插入图片描述

可以看到,有多个使用方式,其中Runnable和Callable我们都已经玩过了,这里我们看一下ForkJoinTask怎么玩:
在这里插入图片描述
可以看出,它需要实现三个方法,用起来还是比较麻烦的,当然你也可以使用RecursiveTask:
在这里插入图片描述
可以看到,这样就只需要实现一个方法就可以了,RecursiveTask继承了ForkJoinTask,从名称就可以看出来,它是用于递归计算的。

这里为了方便阅读,我们就不用匿名内部类了:

class MyTask extends RecursiveTask<Integer> {
    //当前任务计算的开始
    private int start;
    //当前任务计算的结束
    private int end;

    //阈值,如果end-start在阈值以内,那么就不用再去细分任务
    public static final int threshold = 2;

    public MyTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        boolean needFork = (end - start) > threshold;
        if (needFork){
            int middle = (start + end) / 2;
            MyTask leftTask = new MyTask(start, middle);
            MyTask rightTask = new MyTask(middle + 1, end);
            
            //执行子任务
            leftTask.fork();
            rightTask.fork();
            
            //得到子任务执行完成的结果
            Integer leftResult = leftTask.join();
            Integer rightResult = rightTask.join();
            
            sum = leftResult + rightResult;
        }else {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        }
        return sum;
    }
}

然后我们执行main方法:

 // ForkJoinPool实现1-100的求和
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ForkJoinPool pool = new ForkJoinPool();
        ForkJoinTask<Integer> task = pool.submit(new MyTask(1, 100));
        Integer sum = task.get();

        System.out.println(sum);
    }

查看控制台:
在这里插入图片描述

经过实验后我们发现,ForkJoin的缺点也很明显,比如编码复杂度还是比较高的,其次,我们的任务不能抛出检查异常。
项目中实际使用ForkJoinPool的情况并不多,但是搞懂ForkJoinPool对于我们阅读JDK源码还是有很大帮助的,比如parallelStream(),它的底层就是用ForkJoinPool实现的,所以了解一下还是有帮助的。

Executors

Executor是一个创建线程的工厂以及工具,它作为工厂的时候我们前面已经探讨过了,它可以使用Executor.defaultThreadFactory()创建一个默认的线程工厂,除此之外还提供了privilegedThreadFactory。

这里我们详细探讨Executors作为工具的一面,到目前为止,我们前面已经介绍了三种线程池,但是不管哪种线程池,使用的参数还是比较复杂,特别是ThreadPoolExecutor最多可以传七个构造参数,所以Executors考虑到这一点,所以提供了一些工具方法,来帮助我们快速创建一下典型的线程池。

来看一下API:

方法特性
newCachedThreadPool() 返回:ThreadPoolExecutor1.缓存型线程池,先 会查看池中是否有以前创建的线程,有就复用,没有就新建 2.适用于生存周期很短的异步任务
newFixedThreadPool() 返回:ThreadPoolExecutor1.固定线程池,任意时间最多只有固定数目的活动线程存在 2.适用于线程数比较稳定的并发场景
newSingleThreadExecutor 返回:ThreadPoolExecutor1.任意时间池中只有一个线程,保证任务按照指定顺序执行 2.适用于需要严格控制执行顺序的场景
newScheduledThreadPool() 返回:ScheduledThreadPoolExecutor1.创建一个有调度能力的线程池,返回ScheduleThreadPoolExecutor 2.适用于定时任务、延时任务
newWorkStealingPool() 返回:ForkJoinPool1.创建一个ForkJoinPool 2.适用于分而治之、递归计算的场景

虽然如此,但是阿里规约中禁止使用这种方式创建线程池,这是因为线程池本身的参数就是非常复杂的,尽管Executors给我们提供了方便,但事实上很多人没有办法Hold住这些参数,如果你直接使用Executors的话,那么就会很容易忽略掉里面的代码是怎么写的,这会在一些极端的场景可能导致严重的问题,比如你使用newFixedThreadPool:
在这里插入图片描述
可以看到这里面是一个无界队列,可以无限制存储任务,那么极端场景下,如果任务特别多,可能就导致内存溢出的情况。
所以建议还是手动创建线程池。

线程池调优实战

上面我们已经探讨了线程池使用的细节,这里我们详细探讨一下线程池的调优方法。
线程池调优主要分为两个方面:

  • 线程数调优
  • BlockingQueue调优
线程数调优

实际项目中调优线程数是一个比较麻烦事,因为线程数如果过多的话就会导致线程之间竞争过于激烈,从而降低性能,而设置的太小又会无法充分利用计算机的资源,造成资源浪费,并且由于环境是不断变化的,即使是同一个线程池在不同时间的最优线程数也可能会有差异。
好在一般来说可以根据实际的操作因素,计算出一个相对合理的线程数。目前,业界把任务一般分为以下几种:

  • CPU密集型任务
  • IO密集型任务
  • 混合型任务
CPU密集型

一般来说,对于CPU密集型可以把核心线程数设置成N+1,其中N指的是CPU核心数。
CPU核心数可以这样计算:
在这里插入图片描述

在这里插入图片描述
这样就可以看出,我的CPU是八核心的。

这样如果我们的线程池运行的是一个CPU密集型,我们就可以设置成9个核心线程。

那么大家想,为什么把它设置成CPU核心数+1呢,理论上把它设置成CPU核心数性能是最优的,因为没有任何线程切换的开销,同时又可以让每个CPU的核心忙起来,没有任何浪费,这样想当然是没有任何问题,但是,如果某个线程突然出现暂停或者中断的话,那么CPU就会有一个核心处于空闲状态了。
所以我们一般会设置成N+1,这样多出来的线程就可以充分利用CPU的空闲时间。

IO密集型

而对于IO密集型,由于大部分时间是处理IO的,而线程处理IO的过程是不需要占用CPU的,所以处理IO的这段时间,这个线程就可以交给其他任务去使用,因此IO密集型的任务可以多配置一些线程,业界比较认可的经验值是2N。

混合型

我们不难发现,对于IO密集型的任务与CPU密集型的任务核心线程数的调优还是比较简单的,但是实际项目中,纯粹的CPU密集或者IO密集很难遇到,更多的是混合型的任务,那这个时候我们怎么设置线程数量呢?
这里普及一个估算核心线程数的公式:
核心线程数=N * U * (1+WT/ST) 其中:

  • N:CPU核心数
  • U:期待目标CPU利用率
  • WT:线程等待时间
  • ST:线程运行时间

其中这里的N和U还是比较好获取的,但是WT和ST该怎么获取呢?
这里我们普及一个技巧:
启动项目后打开终端:
输入jvisualvm,然后在左侧侧边栏中点击我们刚刚启动的项目:
在这里插入图片描述
然后找到Profiler,点击CPU,这样Java visualVM就会进行一段时间的性能分析。
在这里插入图片描述
等待一段时间分析完成后,我们看一下结果:
在这里插入图片描述
其中自用时间指的是线程的运行时间,也就是公式里的ST;总时间减去自用时间得到的就是WT,然后计算一下就能得到结果了。

BlockingQueue调优

你可以估算一下你的每一个任务运行需要花费多少内存,你的线程池总共花费多少内存,然后计算一下就可以得到BlockingQueue的大小了。
那么我们不难发现,实际项目中,对于混合型的任务,线程池的调优还是比较麻烦的,你需要做一堆的计算才能知道线程的数量,再进行一堆的估算才能知道BlockingQueue需要设置多大。

因此小编在这里提供了一个懒人工具类,用来帮助我们迅速优化线程池。
地址:
https://www.javacodegeeks.com/2012/03/threading-stories-about-robust-thread.html

文章里有一个工具类:

/**
 * A class that calculates the optimal thread pool boundaries. It takes the desired target utilization and the desired
 * work queue memory consumption as input and retuns thread count and work queue capacity.
 * 
 * @author Niklas Schlimm
 * 
 */
public abstract class PoolSizeCalculator {

 /**
  * The sample queue size to calculate the size of a single {@link Runnable} element.
  */
 private final int SAMPLE_QUEUE_SIZE = 1000;

 /**
  * Accuracy of test run. It must finish within 20ms of the testTime otherwise we retry the test. This could be
  * configurable.
  */
 private final int EPSYLON = 20;

 /**
  * Control variable for the CPU time investigation.
  */
 private volatile boolean expired;

 /**
  * Time (millis) of the test run in the CPU time calculation.
  */
 private final long testtime = 3000;

 /**
  * Calculates the boundaries of a thread pool for a given {@link Runnable}.
  * 
  * @param targetUtilization
  *            the desired utilization of the CPUs (0 <= targetUtilization <= 1)
  * @param targetQueueSizeBytes
  *            the desired maximum work queue size of the thread pool (bytes)
  */
 protected void calculateBoundaries(BigDecimal targetUtilization, BigDecimal targetQueueSizeBytes) {
  calculateOptimalCapacity(targetQueueSizeBytes);
  Runnable task = creatTask();
  start(task);
  start(task); // warm up phase
  long cputime = getCurrentThreadCPUTime();
  start(task); // test intervall
  cputime = getCurrentThreadCPUTime() - cputime;
  long waittime = (testtime * 1000000) - cputime;
  calculateOptimalThreadCount(cputime, waittime, targetUtilization);
 }

 private void calculateOptimalCapacity(BigDecimal targetQueueSizeBytes) {
  long mem = calculateMemoryUsage();
  BigDecimal queueCapacity = targetQueueSizeBytes.divide(new BigDecimal(mem), RoundingMode.HALF_UP);
  System.out.println("Target queue memory usage (bytes): " + targetQueueSizeBytes);
  System.out.println("createTask() produced " + creatTask().getClass().getName() + " which took " + mem
    + " bytes in a queue");
  System.out.println("Formula: " + targetQueueSizeBytes + " / " + mem);
  System.out.println("* Recommended queue capacity (bytes): " + queueCapacity);
 }

 /**
  * Brian Goetz' optimal thread count formula, see 'Java Concurrency in Practice' (chapter 8.2)
  * 
  * @param cpu
  *            cpu time consumed by considered task
  * @param wait
  *            wait time of considered task
  * @param targetUtilization
  *            target utilization of the system
  */
 private void calculateOptimalThreadCount(long cpu, long wait, BigDecimal targetUtilization) {
  BigDecimal waitTime = new BigDecimal(wait);
  BigDecimal computeTime = new BigDecimal(cpu);
  BigDecimal numberOfCPU = new BigDecimal(Runtime.getRuntime().availableProcessors());
  BigDecimal optimalthreadcount = numberOfCPU.multiply(targetUtilization).multiply(
    new BigDecimal(1).add(waitTime.divide(computeTime, RoundingMode.HALF_UP)));
  System.out.println("Number of CPU: " + numberOfCPU);
  System.out.println("Target utilization: " + targetUtilization);
  System.out.println("Elapsed time (nanos): " + (testtime * 1000000));
  System.out.println("Compute time (nanos): " + cpu);
  System.out.println("Wait time (nanos): " + wait);
  System.out.println("Formula: " + numberOfCPU + " * " + targetUtilization + " * (1 + " + waitTime + " / "
    + computeTime + ")");
  System.out.println("* Optimal thread count: " + optimalthreadcount);
 }

 /**
  * Runs the {@link Runnable} over a period defined in {@link #testtime}. Based on Heinz Kabbutz' ideas
  * (http://www.javaspecialists.eu/archive/Issue124.html).
  * 
  * @param task
  *            the runnable under investigation
  */
 public void start(Runnable task) {
  long start = 0;
  int runs = 0;
  do {
   if (++runs > 5) {
    throw new IllegalStateException("Test not accurate");
   }
   expired = false;
   start = System.currentTimeMillis();
   Timer timer = new Timer();
   timer.schedule(new TimerTask() {
    public void run() {
     expired = true;
    }
   }, testtime);
   while (!expired) {
    task.run();
   }
   start = System.currentTimeMillis() - start;
   timer.cancel();
  } while (Math.abs(start - testtime) > EPSYLON);
  collectGarbage(3);
 }

 private void collectGarbage(int times) {
  for (int i = 0; i < times; i++) {
   System.gc();
   try {
    Thread.sleep(10);
   } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    break;
   }
  }
 }

 /**
  * Calculates the memory usage of a single element in a work queue. Based on Heinz Kabbutz' ideas
  * (http://www.javaspecialists.eu/archive/Issue029.html).
  * 
  * @return memory usage of a single {@link Runnable} element in the thread pools work queue
  */
 public long calculateMemoryUsage() {
  BlockingQueue<Runnable> queue = createWorkQueue();
  for (int i = 0; i < SAMPLE_QUEUE_SIZE; i++) {
   queue.add(creatTask());
  }
  long mem0 = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
  long mem1 = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
  queue = null;
  collectGarbage(15);
  mem0 = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
  queue = createWorkQueue();
  for (int i = 0; i < SAMPLE_QUEUE_SIZE; i++) {
   queue.add(creatTask());
  }
  collectGarbage(15);
  mem1 = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
  return (mem1 - mem0) / SAMPLE_QUEUE_SIZE;
 }

 /**
  * Create your runnable task here.
  * 
  * @return an instance of your runnable task under investigation
  */
 protected abstract Runnable creatTask();

 /**
  * Return an instance of the queue used in the thread pool.
  * 
  * @return queue instance
  */
 protected abstract BlockingQueue<Runnable> createWorkQueue();

 /**
  * Calculate current cpu time. Various frameworks may be used here, depending on the operating system in use. (e.g.
  * http://www.hyperic.com/products/sigar). The more accurate the CPU time measurement, the more accurate the results
  * for thread count boundaries.
  * 
  * @return current cpu time of current thread
  */
 protected abstract long getCurrentThreadCPUTime();

}

利用这个工具类就可以帮助我们迅速调优线程池了。

其中代码里也提供了一个测试方法,我们玩一下:

public class MyPoolSizeCalculator extends PoolSizeCalculator {

 public static void main(String[] args) {
  MyPoolSizeCalculator calculator = new MyPoolSizeCalculator();
  calculator.calculateBoundaries(
          //CPU目标利用率
          new BigDecimal(1.0),
          //BlockingQueue占用内存大小,byte
          new BigDecimal(100000));
 }

 protected long getCurrentThreadCPUTime() {
  //当前线程占用的总时间
  return ManagementFactory.getThreadMXBean().getCurrentThreadCpuTime();
 }

 protected Runnable creatTask() {
  //计算线程池该配置的核心线程数多大
  return new AsynchronousTask();
 }

 protected BlockingQueue createWorkQueue() {
  //计算线程池该配置的BlockingQueue多大
  return new LinkedBlockingQueue<>();
 }

}

class AsynchronousTask implements Runnable{

 @Override
 public void run() {
  System.out.println(Thread.currentThread().getName());
 }
}

看下运行结果:
在这里插入图片描述
Number of CPU: 8 表示我们的CPU核心数是8
Target utilization: 1表示我们期望的CPU核心占用率是100%
Elapsed time (nanos): 3000000000 表示测试过程已经花费多少纳秒。
Compute time (nanos): 2758064000 表示线程已经运行了多少纳秒
Wait time (nanos): 241936000表示等待了多少纳秒
Formula: 8 * 1 * (1 + 241936000 / 2758064000):公式
* Optimal thread count: 8 算出来的建议线程数是8

当然BlockingQueue的大小也可以估算出来,我们先把下面的打印注释掉:
在这里插入图片描述
再次运行,看最上方的输出:
在这里插入图片描述
可以看到它估算出来一个任务需要花费40byte空间,然后用100000/40 最终得到2500,即建议Queue的大小设置为2500。

有了工具类后,我们就可以很方便的进行线程池调优了。

注意
但是需要注意的是,我们上面介绍的几个公式,不管是N+1也好,2N也好 都仅仅是业界比较认可的经验值公式,并不是说按照这个公式就一定能达到最优,实际项目中一定要结合实际情况进行调优,你可以这样进行操作:

  • 首先根据业务进行评估,评估其中的任务是CPU密集型还是IO密集型或者是混合型的
  • 然后利用公式计算出一个建议值
  • 再进行多次压测,并且去逐步调整线程池大小
  • 然后多次对比,评估出一个表现最好的值
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值