1. 为什么要自定义线程池
首先ThreadPoolExecutor中,一共提供了7个参数,每个参数都是非常核心的属性,在线程池去执行任务时,每个参数都有决定性的作用。
但是如果直接采用JDK提供的方式去构建,可见设置的核心参数最多就两个,这样就会导致对线程池的控制粒度很粗。所以在阿里规范中也推荐自己创建自定义线程池。
自定义构建线程池,可以细粒度的控制线程池,去管理内存的属性,并且针对一些参数的设置可能更好的在后期排查问题。
ThreadPoolExecutor 七大核心参数:
1 2 3 4 5 6 7 |
|
2.ThreadPoolExecutor应用
JDK提供的几种拒绝策略:
- AbortPolicy: 当前拒绝策略会在无法执行任务时,直接抛出一个异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
- CallerRunsPolicy: 当前拒绝策略会在无法执行任务时,将任务交给调用者处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
- DiscardPolicy:当前拒绝策略会在无法执行任务时,直接将任务丢弃
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
- DiscardOldestPolicy: 当前拒绝策略会在无法执行任务时,将阻塞队列中最早的任务丢弃,将当前任务再次交接线程池处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
- 当然也可以自定义拒绝策略,根据自己业务修改实现逻辑, 只需实现 RejectedExecutionHandler 类中的 rejectedExecution 方法。
3. ThreadPoolExecutor的核心属性
线程池的核心属性就是ctl,它会基于ctl拿到线程池的状态以及工作线程个数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
线程池的转换方式:
ThreadPoolExecutor中的execute方法
execute方法是提交任务到线程池的核心方法。
execute源码解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
execute方法流程图:
ThreadPoolExecutor中的addWorker方法
addWorker方法中主要分为两大块:
- 第一块:校验线程池的状态以及工作线程个数
- 第二块:添加工作线程并且启动工作线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
|
线程池为啥要构建空任务的非核心线程?
- 第一个:在 execute 方法中有个判断工作线程是否为0,是就添加一个空任务的非核心线程;
else if (workerCountOf(recheck) == 0) addWorker(null, false);
- 第二个:在工作线程 Worker 启动后,工作线程会运行 runWorker 方法,该方法中有个操作,当工作线程结束之后会执行 processWorkerExit 方法,在这个方法内部又有添加一个空任务的非核心线程;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
综合上诉,Java它有一个这样的场景:
在初始化线程池的时候可能设置线程池的核心线程数为0 或者 设置allowCoreThreadTimeOut(默认false)为ture导致核心线程超时释放,存在没有核心线程的情况。
当我们把任务添加到阻塞队列之后,没有工作线程导致阻塞队列任务堆积,直到后续有新任务加入才会去创建工作线程。
1 2 3 4 5 6 7 8 |
|
综上所述,因此线程池需要构建空任务的非核心线程去处理这种情况。
线程池使用完为什么必须执行shutdown方法或者shutdownNow方法?
- 第一点:在线程池 addWorker 方法中我们可以看到,
线程池启动线程也是基于 Thread 对象去进行的一个 start 方法启动的,像这种它会占用jvm的栈,
所以属于GC Roots 通过垃圾回收的可达性分析算法,这种线程就不能被回收,会一直占用jvm的资源,
因此不能及时的调用 shutdown 或者 shutdownNow 方法,就可能造成内存泄漏问题!!!
- 第二点:线程池启动对象是基于你 Worker 对象内部的 Thread 对象启动的,
当执行Thread对象的 start方法时,它会执行 Worker对象的 run 方法,
该方法中的runWorker 方法传入的是 this 就是当前的Worker对象,
就会导致启动的线程还指向了Worker对象,这个Worker对象是不能回收的,
又因为Worker对象属于线程池的内部类,
导致整个 ThreadPoolExecutor 线程池对象也不会被回收!!!
综上所述,当使用完线程池对象后,没有及时的调用关闭方法,会导致堆内存资源消耗很严重,最后会导致内存泄漏问题!
线程池的核心参数该如何设置?
主要的难点在于任务类型无法控制,比如:
cpu密集型: cpu不断的处理任务,大量的计算等操作。
IO密集型: 不需要cpu一直调度,大多数时间都是等待结果的,如:调用第三方服务等待网络响应、等待IO响应、查询数据库等待数据库响应等等。
混合型:上面两种都会有。
大多数情况都需自己去测试,调试!没有绝对固定的一个公式。可以参考:
N thread = N cpu * U cpu * ( 1 + W / C )
线程数 = cpu的个数 * cpu的利用率 * ( 1 + 等待时间 / 计算时间 ) 注:W/C 是程序运行时 等待时间和计算时间的比值
1 * 100% * (1 + 50% / 50% )= 2
公式只是给定一个调试的初始值,需要自己后续测试调试!