Java线程池的shutdownnow()方法为什么不能停止运行的任务?

在工作中到了一个实际问题,明明已经调用了线程池的shutdownnow()方法,但是线程池中任务依然在继续运行。

加下来先大概介绍一下问题背景,然后给出原因分析,最后给出这个问题的解决办法。

一、问题背景

事情是这样的,在一个线程池中运行着一个周期任务task,这个task的run方法中会执行四个子任务,每个子任务中都会有网络操作。

下面简单用代码进行展示一下。

public class Task implements Runnable{
    private List<Runnable> subtasks = new ArrayList<>();
    
    public boolean addSubtask(Runnable subtask) {
        return subtasks.add(subtask);
    }
    
    @Override
    public void run() {
        for (Runnable subtask: subtasks) {
            subtask.run();
        }
    }
}

子任务的代码:

public class Subtask implements Runnable {
    private String operation; // 子任务要执行的操作

    public Subtask(String operation) {
        this.operation = operation;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " start to execute " + this.operation);
        try {
            Thread.sleep(2000); // 用sleep来模拟一下实际的任务处理
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " finish to execute " + this.operation);
    }
}

下面就结合具体的微服务注册场景来展现一个生动的例子:

public class Example {
    private static ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1);

    public static void main(String[] args) throws InterruptedException {
        Runnable microserviceRegisterTask = new Subtask("Register microservice");
        Runnable microserviceInstanceRegisterTask = new Subtask("Register microservice instance");
        Runnable serviceWatchTask = new Subtask("Watch all service changes event");
        Runnable heartBeatTask = new Subtask("Instance heart beat");

        Task task = new Task();
        task.addSubtask(microserviceRegisterTask);
        task.addSubtask(microserviceInstanceRegisterTask);
        task.addSubtask(serviceWatchTask);
        task.addSubtask(heartBeatTask);

        threadPool.scheduleAtFixedRate(task, 0, 10, TimeUnit.SECONDS);

        Thread.sleep(11*1000);
        System.out.println("The thread pool is shut down at now.");
        threadPool.shutdownNow();
    }
}

运行结果:

pool-1-thread-1 start to execute Register microservice
pool-1-thread-1 finish to execute Register microservice
pool-1-thread-1 start to execute Register microservice instance
pool-1-thread-1 finish to execute Register microservice instance
pool-1-thread-1 start to execute Watch all service changes event
pool-1-thread-1 finish to execute Watch all service changes event
pool-1-thread-1 start to execute Instance heart beat
pool-1-thread-1 finish to execute Instance heart beat
pool-1-thread-1 start to execute Register microservice
java.lang.InterruptedException: sleep interrupted
The thread pool is shut down at now.
	at java.lang.Thread.sleep(Native Method)
pool-1-thread-1 finish to execute Register microservice
pool-1-thread-1 start to execute Register microservice instance
	at com.mucao.showcase.Subtask.run(Subtask.java:14)
	at com.mucao.showcase.Task.run(Task.java:16)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:748)
pool-1-thread-1 finish to execute Register microservice instance
pool-1-thread-1 start to execute Watch all service changes event
pool-1-thread-1 finish to execute Watch all service changes event
pool-1-thread-1 start to execute Instance heart beat
pool-1-thread-1 finish to execute Instance heart beat

Process finished with exit code 0

可以看到,我故意在例子中卡了一个时间点,当执行注册服务信息的时候,调用了线程池的shutdownnow方法,但是随后的实例注册等任务依然在执行。运行结果中也看到了线程池中任务收到了Interrupt信号,但是为什么没有立即停止下来呢?

二、原因分析

细节里面藏着魔鬼,真正的原因就在于InterruptedException异常了,Thread.sleep(...)方法(生产环境可能是CountDownLatch的wait方法)在执行的过程中如果被中断,那么会立即抛出InterruptedException异常并且清楚掉线程的Interrupted状态标识。也即是说,在执行子任务microserviceRegisterTask时,收到了中断,并且也是立即停止了任务的执行,并且Thread.sleep(...)抛出异常,但同时也清除了Interrupted状态标识,那么Task.run()中的for循环会继续运行子任务microserviceInstanceRegisterTask。

下面是javadoc对于Thread.sleep方法的说明

public static void sleep(long millis) throws InterruptedException

Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers. The thread does not lose ownership of any monitors.

  • Parameters:

millis - the length of time to sleep in milliseconds

  • Throws:

IllegalArgumentException - if the value of millis is negative

InterruptedException - if any thread has interrupted the current thread. The interrupted status of the current thread is cleared when this exception is thrown.

三、解决方式

问题原因清楚了,那么其实问题就好解决了。

方式(1) 继续向上抛异常。
我当时首先想到的就是让InterruptedException异常继续向上抛,这样在Task的for循环中就可以捕捉,但是因为发生异常的地方时子任务的run()方法,Runnable接口的run方法是没有声明抛出异常的,所以在这儿是没有办法将异常向上抛出的。

方式(2) 重新设置线程的Interrupted状态标识。
我只要在子任务的catch块中重新设置线程的Interrupted状态标识,这样在Task中就可以检测到中断标识,然后就可以停止for循环了。

public class Subtask implements Runnable {
	......
    @Override
    public void run() {
	   ......
        try {
            Thread.sleep(2000); // 用sleep来模拟一下实际的任务处理
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
	   ......
    }
}
public class Task implements Runnable{
	......
    @Override
    public void run() {
        for (Runnable subtask: subtasks) {
            if (Thread.interrupted()) {
                return;
            }
            subtask.run();
        }
    }
}

在技术上来说是可以这样的,但是考虑到其实有点儿改变了JDK的默认行为,如果在整个业务代码中有其他地方就是需要抛出InterruptedException的时候并且清除掉当前线程的interrupted状态标识,那么上面这么做可能会引起不可预知的行为。

方式(3) 在调用线程池的shutdownnow方法的同时明确地向运行的任务发送退出信号。
方式3相对于方式2更加的明确直观,而且更加简单可靠。
代码修改后,如下所示:

public class Task implements Runnable{
    private List<Runnable> subtasks = new ArrayList<>();
    
    private volatile boolean stopped = false;
    
    public boolean addSubtask(Runnable subtask) {
        return subtasks.add(subtask);
    }
    
    public void stop() {
        this.stopped = true;
    }

    @Override
    public void run() {
        for (Runnable subtask: subtasks) {
            if (this.stopped) {
                System.out.println("The task is stopped.");
                return;
            }
            subtask.run();
        }
    }
}
public class Example {
    private static ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1);

    public static void main(String[] args) throws InterruptedException {
        Runnable microserviceRegisterTask = new Subtask("Register microservice");
        Runnable microserviceInstanceRegisterTask = new Subtask("Register microservice instance");
        Runnable serviceWatchTask = new Subtask("Watch all service changes event");
        Runnable heartBeatTask = new Subtask("Instance heart beat");

        Task task = new Task();
        task.addSubtask(microserviceRegisterTask);
        task.addSubtask(microserviceInstanceRegisterTask);
        task.addSubtask(serviceWatchTask);
        task.addSubtask(heartBeatTask);

        threadPool.scheduleAtFixedRate(task, 0, 10, TimeUnit.SECONDS);

        Thread.sleep(11*1000);
        System.out.println("The thread pool is shut down at now.");
        task.stop();
        threadPool.shutdownNow();
    }
}

执行结果如下:

pool-1-thread-1 start to execute Register microservice
pool-1-thread-1 finish to execute Register microservice
pool-1-thread-1 start to execute Register microservice instance
pool-1-thread-1 finish to execute Register microservice instance
pool-1-thread-1 start to execute Watch all service changes event
pool-1-thread-1 finish to execute Watch all service changes event
pool-1-thread-1 start to execute Instance heart beat
pool-1-thread-1 finish to execute Instance heart beat
pool-1-thread-1 start to execute Register microservice
The thread pool is shut down at now.
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.mucao.showcase.Subtask.run(Subtask.java:14)
	at com.mucao.showcase.Task.run(Task.java:26)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:748)
pool-1-thread-1 finish to execute Register microservice
The task is stopped.

Process finished with exit code 0

可以看到,确实在执行注册服务任务被打断后没有再继续执行实例注册任务了。

四、总结

调用shutdownnow()方法退出线程池时,线程池会向正在运行的任务发送Interrupt,任务中的阻塞操作会响应这个中断并抛出InterruptedException,但同时会清除线程的Interrupted 状态标识,导致后续流程感知不到线程的中断了。要想立即停止线程池中任务最好的方式就是直接向任务传递退出信号。

解决完这个问题也意识到现实工作中对于知识准确度的要求还是挺高的,稍不留神就会出问题,以后学东西的时候还是要通过实践来检验学习效果,这样才能掌握的更加准确。