线程池的优雅关闭实践
平时开发中,大家更多的关注的是线程池的创建、任务的提交和执行。往往会忽略线程池的关闭,甚至忘记调用shutdown()
方法,导致内存溢出。大多知道需要调用shutdown()关闭线程池,也少研究其真正的关闭过程。
首先看源码中的一句注释:
A pool that is no longer referenced in a program and has no remaining threads will be shutdown automatically.
如果程序中不再持有线程池的引用,并且线程池中没有线程时,线程池将会自动关闭。
线程池自动关闭的两个条件:1、线程池的引用不可达;2、线程池中没有线程;
这里对于条件2解释一下,线程池中没有线程是指线程池中的所有线程都已运行完自动消亡。然而我们常用的FixedThreadPool的核心线程没有超时策略,所以并不会自动关闭。
展示两种不同线程池 不关闭 的情况:
1、FixedThreadPool 示例
1
2
3
4
5
6
7
| public static void main(String[] args) {
while(true) {
ExecutorService executorService = Executors.newFixedThreadPool(8);
executorService.execute(() -> System.out.println("running"));
executorService = null;
}
}
|
输出结果:
1
2
3
4
5
6
7
8
9
| running
......
running
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:714)
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:950)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1357)
at test.PoolTest.main(PoolTest.java:29)
|
因为FixedThreadPool的核心线程不会自动超时关闭,使用时必须在适当的时候调用shutdown()方法。
2、 CachedThreadPool 示例
1
2
3
4
5
6
7
8
9
10
| public static void main(String[] args) {
while(true) {
// 默认keepAliveTime为 60s
ExecutorService executorService = Executors.newCachedThreadPool();
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
// 为了更好的模拟,动态修改为1纳秒
threadPoolExecutor.setKeepAliveTime(1, TimeUnit.NANOSECONDS);
threadPoolExecutor.execute(() -> System.out.println("running"));
}
}
|
输出结果:
1
2
3
4
5
6
| running
running
running
running
running
......
|
CachedThreadPool 的线程 keepAliveTime 默认为 60s ,核心线程数量为 0 ,所以不会有核心线程存活阻止线程池自动关闭。 详见 线程池之ThreadPoolExecutor构造 ,为了更快的模拟,构造后将 keepAliveTime 修改为1纳秒,相当于线程执行完马上会消亡,所以线程池可以被回收。实际开发中,如果CachedThreadPool 确实忘记关闭,在一定时间后是可以被回收的。但仍然建议显示关闭。
然而,线程池关闭的意义不仅仅在于结束线程执行,避免内存溢出,因为大多使用的场景并非上述示例那样 朝生夕死。线程池一般是持续工作的全局场景,如数据库连接池。
本文更多要讨论的是当线程池调用shutdown方法后,会经历些什么?思考一下几个问题:
- 是否可以继续接受新任务?继续提交新任务会怎样?
- 等待队列里的任务是否还会执行?
- 正在执行的任务是否会立即中断?
问题1:是否可以继续接受新任务?继续提交新任务会怎样?
1
2
3
4
5
6
| public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 4, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
executor.execute(() -> System.out.println("before shutdown"));
executor.shutdown();
executor.execute(() -> System.out.println("after shutdown"));
}
|
输出结果如下:
1
2
3
4
5
6
| before shutdown
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task PoolTest$$Lambda$2/142257191@3e3abc88 rejected from java.util.concurrent.ThreadPoolExecutor@6ce253f1[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 1]
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 PoolTest.main(PoolTest.java:12)
|
当线程池关闭后,继续提交新任务会抛出异常。这句话也不够准确,不一定是抛出异常,而是执行拒绝策略,默认的拒绝策略是抛出异常。可参见 线程池之ThreadPoolExecutor构造 里面自定义线程池的例子,自定义了忽略策略,但被拒绝时并没有抛出异常。
问题2:等待队列里的任务是否还会执行?
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
| public class WaitqueueTest {
public static void main(String[] args) {
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
for(int i = 1; i <= 100 ; i++){
workQueue.add(new Task(String.valueOf(i)));
}
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, workQueue);
executor.execute(new Task("0"));
executor.shutdown();
System.out.println("workQueue size = " + workQueue.size() + " after shutdown");
}
static class Task implements Runnable{
String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
for(int i = 1; i <= 10; i++){
System.out.println("task " + name + " is running");
}
System.out.println("task " + name + " is over");
}
}
}
|
这个demo解释一下,我们用LinkedBlockingQueue构造了一个线程池,在线程池启动前,我们先将工作队列填充100个任务,然后执行task 0
后立即shutdown()
线程池,来验证线程池关闭队列的任务运行状态。
输出结果如下:
1
2
3
4
5
6
7
8
| ......
task 0 is running
task 0 is over
workQueue size = 100 after shutdown //表示线程池关闭后,队列任然有100个任务
task 1 is running
......
task 100 is running
task 100 is over
|
从结果中我们可以看到,线程池虽然关闭,但是队列中的任务任然继续执行,所以用 shutdown()
方式关闭线程池时需要考虑是否是你想要的效果。
如果你希望线程池中的等待队列中的任务不继续执行,可以使用shutdownNow()
方法,将上述代码进行调整,如下:
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
| public class WaitqueueTest {
public static void main(String[] args) {
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
for(int i = 1; i <= 100 ; i++){
workQueue.add(new Task(String.valueOf(i)));
}
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, workQueue);
executor.execute(new Task("0"));
// shutdownNow有返回值,返回被抛弃的任务list
List<Runnable> dropList = executor.shutdownNow();
System.out.println("workQueue size = " + workQueue.size() + " after shutdown");
System.out.println("dropList size = " + dropList.size());
}
static class Task implements Runnable{
String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
for(int i = 1; i <= 10; i++){
System.out.println("task " + name + " is running");
}
System.out.println("task " + name + " is over");
}
}
}
|
输出结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
| task 0 is running
workQueue size = 0 after shutdown
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
dropList size = 100
task 0 is over
|
从上述输出可以看到,只有任务0执行完毕,其他任务都被drop掉了,dropList的size为100。通过dropList我们可以对未处理的任务进行进一步的处理,如log记录,转发等;
问题3:正在执行的任务是否会立即中断?
要验证这个问题,需要对线程的 interrupt 方法有一定了解。
推荐阅读 ——线程中断机制
关于 interrupt 方法:
首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。
所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
而 Thread.interrupt 的作用其实也不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。
具体来说,当对一个线程,调用 interrupt() 时,
① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。
② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。
interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。也就是说,一个线程如果有被中断的需求,那么就可以这样做。
① 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
② 在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。)
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
| public class InteruptTest {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
executor.execute(new Task("0"));
Thread.sleep(1);
executor.shutdown();
System.out.println("executor has been shutdown");
}
static class Task implements Runnable {
String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 1; i <= 100 && !Thread.interrupted(); i++) {
Thread.yield();
System.out.println("task " + name + " is running, round " + i);
}
}
}
}
|
输出结果如下:
1
2
3
4
5
6
7
8
9
| task 0 is running, round 1
task 0 is running, round 2
task 0 is running, round 3
......
task 0 is running, round 28
executor has been shutdown
......
task 0 is running, round 99
task 0 is running, round 100
|
为了体现在任务执行中打断,在主线程进行短暂 sleep , task 中 调用 Thread.yield() ,出让时间片。从结果中可以看到,线程池被关闭后,正在运行的任务没有被 interrupt。说明shutdown()
方法不会 interrupt 运行中线程。再将其改修改为shutdownNow()
后输出结果如下:
1
2
3
4
5
6
7
8
| task 0 is running, round 1
task 0 is running, round 2
......
task 0 is running, round 56
task 0 is running, round 57
task 0 is running, round 58
task 0 is running, round 59
executor has been shutdown
|
修改为shutdownNow()
后,task任务没有执行完,执行到中间的时候就被 interrupt 后没有继续执行了。
总结,想要正确的关闭线程池,并不是简单的调用shutdown方法那么简单,要考虑到应用场景的需求,如何拒绝新来的请求任务?如何处理等待队列中的任务?如何处理正在执行的任务?想好这几个问题,在确定如何优雅而正确的关闭线程池。
PS:线程被 interrupt 后,需要再run方法中单独处理 interrupted 状态,interrupt 更类似一个标志位,不会直接打断线程的执行。
附录
线程启动原理
线程中断机制
多线程实现方式
FutureTask实现原理
线程池之ThreadPoolExecutor概述
线程池之ThreadPoolExecutor使用
线程池之ThreadPoolExecutor状态控制
线程池之ThreadPoolExecutor执行原理
线程池之ScheduledThreadPoolExecutor概述
线程池之ScheduledThreadPoolExecutor调度原理
线程池的优雅关闭实践
作者:徐志毅
链接:https://www.jianshu.com/p/bdf06e2c1541
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。