瑞瑞哥的博客

关闭线程池时Thread.isInterrupt()注意事项

关闭线程池时Thread.isInterrupted()注意事项

今天在研究方案时随手写了个小demo,想要验证线程和任务的控制问题,然后踩了个小坑,虽然这个根因是自己没看过相关源码导致的,但是还是决定记下来,因为我搜这个问题的时候,百度谷歌都没有相关答案。

先上结论

如果在线程池中,投递了线程(这里指的是java.lang.Thread,以及它的派生类),那么使用当想要判断线程是否中断时,记得要用Thread.currentThread().isInterrupted(),而不是用this.isInterrupted()

源码

目录结构如下:

1
2
3
Main.java
ExecutorCallerThread.java
ExecutorWorkingThread.java

内容如下:
Main.java是主线程,主线程会起一个线程: ExecutorCallerThread,这个线程里面会起一个线程池。线程池里面被提交了4个同样的任务线程(ExecutorWorkingThread),但是线程池大小为2,任务却有4个,且每个任务是无限执行、不会自然退出的,那么意味着只有两个任务能得到执行,另外两个在排队。

上述状态达到后(比如程序开始1秒),主线程开始停掉 ExecutorCallerThreadExecutorCallerThread会尝试用ExecutorService.shutdownNow()把线程池里面的进程也停掉。

期望的结果:
ExecutorCallerThread退出
线程池中的4个ExecutorWorkingThread,不管是正在执行的2个,还是正在排队的2个,都退出。

Main.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.ruiruigeblog.threadtest;

public class Main {

public static void main(String[] args) throws InterruptedException {
ExecutorCallerThread callerThread = new ExecutorCallerThread();
callerThread.start();

// 等1秒,等各种子线程、子线程的子线程都开始工作,然后再尝试停止它们
Thread.sleep(1000);
// 打断子线程
callerThread.interrupt();

while (true) {
Thread.sleep(1000);
}
}
}

ExecutorCallerThread.java:

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
package com.ruiruigeblog.threadtest;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ExecutorCallerThread extends Thread {
@Override
public void run() {
// 线程池大小为2
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 但是我们缺提交4个任务,且4个任务都是死循环、永远无法正常退出的,那么意味着有2个线程永远无法被线程池调度执行
for (int i=0; i<4; i++){
// ExecutorWorkingThread 里面的run()方法是无法正常退出的
executorService.submit(new ExecutorWorkingThread());
}

try {
executorService.awaitTermination(1000, TimeUnit.MINUTES);
} catch (InterruptedException e) {
// 当发生这个异常后,说明本线程本Main线程打断,那么要停止线程池里的线程,然后退出。
// shutdownNow() 方法会停止接受新任务,停止排队中的任务,打断正在执行的任务
executorService.shutdownNow();
}
System.out.println("caller thread exit");
}
}

ExecutorWorkingThread.java:

1
2
3
4
5
6
7
8
9
10
11
12
package com.ruiruigeblog.threadtest;

public class ExecutorWorkingThread extends Thread {
@Override
public void run() {
// 本线程是死循环,除非被打断,否则无法正常退出
while (!isInterrupted()) {
System.out.println(Thread.currentThread().getName() + ": is working");
}
System.out.println(Thread.currentThread().getName() + ": exit by interrupt");
}
}

运行与分析

结果运行之后,程序一直运行,各种线程并没有停下来:
无法正常退出

这是怎么回事呢?翻了下源码才发现自己有点脑残了。。。

首先,我的思路是大致没错的:

  1. 主线程interrupt ExecutorCallerThread
  2. ExecutorCallerThread当时应该阻塞与执行:ExecutorService.awaitTermination(long timeout, TimeUnit unit),等待线程池里的任务执行完毕,阻塞结束。
  3. 当步骤1发生是,步骤2里面的ExecutorService.awaitTermination(long timeout, TimeUnit unit)阻塞会停止,收到一个InterruptedException,我们catch住这个异常后,通过shutdownNow() 方法:
    1. 停止接受新任务:这点没问题,本来就没有新任务提交过来
    2. 停止排队中的任务:这点也没问题,2个未开始的任务给踢掉
    3. 打断正在执行的任务:对于正在执行的2个任务,线程池会进行打断,然后我的工作线程会发现打断,自己退出

那么一切看起来都没问题,问题出在哪里呢?

答案就是:
ExecutorService.submit方法,实际上是Future<?> submit(Runnable task);,它接收的是一个Runnable对象,也把它当作Runnable来对待的。接收后,在线程池内部,创建一个新的Thread,把Runnable当作参数,传递给Thread的构造函数去了。

注意,这里我用的是Executors.newFixedThreadPool(),因此返回的是java.util.concurrent.ThreadPoolExecutor

具体代码如下:
java.util.concurrent.ThreadPoolExecutor$Worker

1
2
3
4
5
6
7
8
9
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}

可以看到,调用链如下:

1
2
3
4
5
AbstractExecutorService.submit(Runnable):
java.util.concurrent.ThreadPoolExecutor.execute(Runnable):
java.util.concurrent.ThreadPoolExecutor.addWorker(Runnable, boolean):
java.util.concurrent.ThreadPoolExecutor$Worker()
this.thread = getThreadFactory().newThread(this);

换句话说,我们自己的ExecutorWorkingThread,根本就没有被创建,被创建的是另一个线程,它包住了我们的线程。那么当我们调用this.isInterrupted()的时候,返回的自然是false,因为线程池interrupt的,是那个包装thread。

当我们把ExecutorWorkingThread.java中的:

1
while (!isInterrupted())

改成:

1
while (!Thread.currentThread.isInterrupted())

结果就正确了:
正常退出

另外,如果投递的直接是Runnable,也不会有这个问题,因为Runnable自身就没有isInterrupted()方法,这也可以扯到向上造型向下造型什么的,那个可能就扯远了。

基本功还是要扎实啊,晕