线程池
线程池顾名思义就是一个放线程的池子,使用线程池的好处有很多:
- 重用已存在的线程
- 控制并发
- 功能强大
从某种意义上来讲,线程池是一种特殊的对象池,因此你也可以使用对象池来自己实现一个线程池,当然一般情况下我们是不需要自己实现线程池的,使用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()方法 |
DelayQueue | 无 | 1.其中的元素必须实现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() 返回:ThreadPoolExecutor | 1.缓存型线程池,先 会查看池中是否有以前创建的线程,有就复用,没有就新建 2.适用于生存周期很短的异步任务 |
newFixedThreadPool() 返回:ThreadPoolExecutor | 1.固定线程池,任意时间最多只有固定数目的活动线程存在 2.适用于线程数比较稳定的并发场景 |
newSingleThreadExecutor 返回:ThreadPoolExecutor | 1.任意时间池中只有一个线程,保证任务按照指定顺序执行 2.适用于需要严格控制执行顺序的场景 |
newScheduledThreadPool() 返回:ScheduledThreadPoolExecutor | 1.创建一个有调度能力的线程池,返回ScheduleThreadPoolExecutor 2.适用于定时任务、延时任务 |
newWorkStealingPool() 返回:ForkJoinPool | 1.创建一个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密集型或者是混合型的
- 然后利用公式计算出一个建议值
- 再进行多次压测,并且去逐步调整线程池大小
- 然后多次对比,评估出一个表现最好的值