目录
- 一、简介
- 二、线程池任务场景
- 场景一:提交5个任务,执行总耗时500ms
- 场景二:提交10个任务,执行总耗时500ms
- 场景三:提交11个任务,执行总耗时1000ms
- 场景四:提交20个任务,执行总耗时1000ms
- 场景五:提交30个任务,执行总耗时1500ms
- 场景六:提交40个任务,执行总耗时2000ms
- 场景七:提交41个任务,执行总耗时2000ms
- 场景八:提交45个任务,执行总耗时1500ms
- 场景九:提交50个任务,执行总耗时1500ms
- 场景十:提交51个任务,执行总耗时1500ms
- 三、总结
线程池原理详见:Java基础——深入理解Java线程池
一、简介
网上有很多关于 线程池原理
的讲解,理论说的多了你也不一定记得住,也不一定理解的了,下面我通过一个DEMO,加上多个任务场景,让你能够直观的理解线程池的工作流程。
二、线程池任务场景
线程池有几个关键参数:核心线程数、最大线程数、回收时间、队列等。
我们DEMO使用自定义线程池方式来讲解线程池的工作流程,代码如下:
package com.example.springbootdemo.util;import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;public class ThreadPoolTest {// 任务数private static int taskCount = 50;// 实际完成任务数private static AtomicInteger taskCountExecuted;public static void main(String[] args) {init();}private static void init(){taskCountExecuted = new AtomicInteger(0);ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, // 核心线程数20, // 最大线程数5, // 非核心线程回收超时时间TimeUnit.SECONDS, // 超时时间单位new ArrayBlockingQueue<>(30) // 任务队列);System.out.println("任务总数 = [" + taskCount + "]个");long start = System.currentTimeMillis();for(int i=0; i<taskCount; i++){Runnable runnable = new Runnable() {@Overridepublic void run() {try{Thread.sleep(500);System.out.println("已执行第 [" + taskCountExecuted.addAndGet(1) + "] 个任务");}catch (Exception ex){ex.printStackTrace();}}};try{// 默认拒绝策略会报错threadPoolExecutor.execute(runnable);}catch (Exception ex){ex.printStackTrace();taskCount = threadPoolExecutor.getActiveCount() + threadPoolExecutor.getQueue().size();}}long end = 0;while (threadPoolExecutor.getCompletedTaskCount() < taskCount){end = System.currentTimeMillis();}System.out.println("[" + taskCountExecuted + "]个任务执行总耗时 = [" + (end - start) + "]ms");threadPoolExecutor.shutdown();}
}
说明:我们DEMO中新建了个线程池,核心线程数10,最大线程数20,任务队列容量30。
执行每个任务场景只需修改 taskCount
值即可。
场景一:提交5个任务,执行总耗时500ms
taskCount = 5;
执行结果:
任务总数 = [5]个
已执行第 [2] 个任务
已执行第 [3] 个任务
已执行第 [4] 个任务
已执行第 [1] 个任务
已执行第 [5] 个任务
[5]个任务执行总耗时 = [506]ms
分析:
核心线程数为10,也就是说有10个线程数长期处于活动状态,即来任务立马就能执行,任务数5 < 核心线程数10,所以,5个任务立马执行完成,且是多线程并行执行,所以任务执行总耗时 = 500ms
注:我们日志输出的是 505ms ,因为代码执行也是要花时间的。
场景二:提交10个任务,执行总耗时500ms
taskCount = 10;
执行结果:
任务总数 = [10]个
已执行第 [2] 个任务
已执行第 [8] 个任务
已执行第 [5] 个任务
...
已执行第 [3] 个任务
已执行第 [9] 个任务
已执行第 [10] 个任务
[10]个任务执行总耗时 = [507]ms
分析:
任务数10 <= 核心线程数10,10个任务立马执行完成,所以任务执行总耗时 = 500ms。
场景三:提交11个任务,执行总耗时1000ms
taskCount = 11;
执行结果:
任务总数 = [11]个
已执行第 [1] 个任务
已执行第 [3] 个任务
已执行第 [4] 个任务
...
已执行第 [8] 个任务
已执行第 [7] 个任务
已执行第 [11] 个任务
[11]个任务执行总耗时 = [1009]ms
分析:
任务执行总耗时 = 1000ms,别惊讶,这里是很多人没有搞懂线程池运行机制的关键点,虽然任务只多个一个,但是地11个任务不是立马执行的,核心线程数为10,第11个任务会进入到任务队列,等核心线程有空出来后会从任务队列中取出任务再来执行,因此任务总耗时 = 1000ms。
场景四:提交20个任务,执行总耗时1000ms
taskCount = 20;
执行结果:
任务总数 = [20]个
已执行第 [5] 个任务
已执行第 [4] 个任务
已执行第 [2] 个任务
...
已执行第 [16] 个任务
已执行第 [19] 个任务
已执行第 [20] 个任务
[20]个任务执行总耗时 = [1010]ms
分析:
任务执行总耗时 = 1000ms,此处与前一个场景一样,第11到第20共10个任务会进入到任务队列,等核心线程有空出来后会从任务队列中取出任务再来执行,因为有10个核心线程,前10个任务执行完成后,任务队列中的10个任务正好由空出的10个核心线程来执行,因此任务总耗时 = 1000ms。
场景五:提交30个任务,执行总耗时1500ms
taskCount = 30;
执行结果:
任务总数 = [30]个
已执行第 [2] 个任务
已执行第 [6] 个任务
已执行第 [3] 个任务
...
已执行第 [23] 个任务
已执行第 [25] 个任务
已执行第 [30] 个任务
[30]个任务执行总耗时 = [1514]ms
分析:
任务执行总耗时 = 1500ms,此处与前一个场景一样,第11到第30共20个任务会进入到任务队列,等核心线程有空出来后会从任务队列中取出任务再来执行,因为有10个核心线程,前10个任务执行完成后,从任务队列中取出10个任务由空出的10个核心线程来执行,执行完后在取出10个任务来执行,因此任务总耗时 = 1500ms。
场景六:提交40个任务,执行总耗时2000ms
taskCount = 40;
执行结果:
任务总数 = [40]个
已执行第 [1] 个任务
已执行第 [2] 个任务
已执行第 [4] 个任务
...
已执行第 [32] 个任务
已执行第 [34] 个任务
已执行第 [40] 个任务
[40]个任务执行总耗时 = [2016]ms
分析:
任务执行总耗时 = 2000ms,此处与前一个场景一样,这里不再过多解释。
场景七:提交41个任务,执行总耗时2000ms
taskCount = 41;
执行结果:
任务总数 = [41]个
已执行第 [1] 个任务
已执行第 [2] 个任务
已执行第 [3] 个任务
...
已执行第 [40] 个任务
已执行第 [37] 个任务
已执行第 [41] 个任务
[41]个任务执行总耗时 = [2016]ms
分析:
任务执行总耗时 = 2000ms,这里重点来了,我们知道第11到第40共30个任务进入到了任务队列中(任务队列大小为30),第41个任务就创建了一个非核心线程(最大线程数20 - 核心线程数10 = 10个非核心线程)来执行,此时线程池中的活跃线程数为11,第一批任务执行完后,会从任务队列中取出11个任务来执行,那就是 11 + 11 + 11 + 8 = 500ms * 4 = 2000ms。
场景八:提交45个任务,执行总耗时1500ms
taskCount = 45;
执行结果:
任务总数 = [45]个
已执行第 [3] 个任务
已执行第 [4] 个任务
...
已执行第 [43] 个任务
已执行第 [42] 个任务
已执行第 [45] 个任务
[45]个任务执行总耗时 = [1516]ms
分析:
任务执行总耗时 = 1500ms,此处与前一个场景一样,我们知道第11到第40共30个任务进入到了任务队列中(任务队列大小为30),第41到第45共5个任务就创建了5个非核心线程(最大线程数20 - 核心线程数10 = 10个非核心线程)来执行,此时线程池中的活跃线程数为15,第一批任务执行完后,会从任务队列中取出15个任务来执行,那就是 15 + 15 + 15 = 500ms * 3 = 1500ms。
场景九:提交50个任务,执行总耗时1500ms
taskCount = 50;
执行结果:
任务总数 = [50]个
已执行第 [1] 个任务
已执行第 [9] 个任务
已执行第 [12] 个任务
...
已执行第 [45] 个任务
已执行第 [44] 个任务
已执行第 [50] 个任务
[50]个任务执行总耗时 = [1515]ms
分析:
任务执行总耗时 = 1500ms,此处与前一个场景一样,我们知道第11到第40共30个任务进入到了任务队列中(任务队列大小为30),第41到第50共10个任务就创建了10个非核心线程(最大线程数20 - 核心线程数10 = 10个非核心线程)来执行,此时线程池中的活跃线程数为20,第一批任务执行完后,会从任务队列中取出20个任务来执行,那就是 20 + 20 + 10 = 500ms * 3 = 1500ms。
场景十:提交51个任务,执行总耗时1500ms
taskCount = 10;
执行结果:
任务总数 = [51]个
java.util.concurrent.RejectedExecutionException: Task com.example.springbootdemo.util.ThreadPoolTest$1@682a0b20 rejected from java.util.concurrent.ThreadPoolExecutor@3d075dc0[Running, pool size = 20, active threads = 20, queued tasks = 30, completed tasks = 0]at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)at com.example.springbootdemo.util.ThreadPoolTest.init(ThreadPoolTest.java:48)at com.example.springbootdemo.util.ThreadPoolTest.main(ThreadPoolTest.java:16)
已执行第 [3] 个任务
已执行第 [5] 个任务
...
已执行第 [49] 个任务
已执行第 [48] 个任务
已执行第 [50] 个任务
[50]个任务执行总耗时 = [1514]ms
分析:
任务执行总耗时 = 1500ms,此处与前一个场景一样,由于线程池同时最大能接收50个任务(最大线程数20 + 任务队列大小30 = 50),所以第51个任务被拒绝了(线程池使用默认拒绝策略AbortPolicy),抛出了异常,DEMO中使用了try-catch捕获到了。
三、总结
-
任务数
<=核心线程数
,线程池中工作线程数
=任务数
; -
核心线程数
<任务数
<= (核心线程数 + 队列容量
)时,线程池中工作线程数
=核心线程数
; -
(
核心线程数 + 队列容量
) <任务数
<= (最大线程数 + 队列容量
)时,线程池中工作线程数
= (任务数 - 队列容量
);