如何优雅关停线程池?

87 阅读4分钟

1. 介绍几个API

如果你想探究这些API的原理,可以查看 线程池源码解析+设计思想+线程池监控框架设计 - 掘金 (juejin.cn)

0. 准备

public class Task implements Runnable {

    private static final Logger log = LoggerFactory.getLogger(Task.class);
    @Getter
    private final int id;

    public Task(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println("第 " + id + " 次开始执行");
        } catch (InterruptedException e) {
            // 重置打断标记
            Thread.currentThread().interrupt();
            if (Thread.currentThread().isInterrupted()) {
                log.info("第 {} 次任务被打断了", id);
            }
        }
    }
}

1. shutDown()

该API会关闭线程池,拒绝提交新的任务。但是正在执行的任务或者放在任务队列里面的任务不会中断。

    public static void shutdown() {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            if (i == 4) {
                threadPool.shutdown();
            }
            try {
                threadPool.execute(new Task(i + 1));
            } catch (Exception e) {
                log.info("第 {} 次任务被拒绝", i + 1);
            }
        }
        
        //17:40:47.545 [main] INFO com.hdu.Main - 第 5 次任务被拒绝
        //17:40:47.547 [main] INFO com.hdu.Main - 第 6 次任务被拒绝
        //17:40:47.547 [main] INFO com.hdu.Main - 第 7 次任务被拒绝
        //17:40:47.547 [main] INFO com.hdu.Main - 第 8 次任务被拒绝
        //17:40:47.547 [main] INFO com.hdu.Main - 第 9 次任务被拒绝
        //17:40:47.547 [main] INFO com.hdu.Main - 第 10 次任务被拒绝
        //第 1 次开始执行
        //第 2 次开始执行
        //第 3 次开始执行
        //第 4 次开始执行
    }

2. shutDownNow()

该API会关闭线程池,拒绝提交新的任务。并且,正在执行的任务会被打断,任务队列里面的任务也会返回。

    private static void shutdownNow() {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            try {
                threadPool.execute(new Task(i + 1));
            } catch (Exception e) {
                log.info("第 {} 次任务被拒绝", i + 1);
            }
            if (i == 4) {
                List<Runnable> remainTasks = threadPool.shutdownNow();
                for (int j = 0; j < remainTasks.size(); j++) {
                    Runnable task = remainTasks.get(j);
                    if (task instanceof Task) {
                        log.info("第 {} 次任务从任务队列里面移除", ((Task) task).getId());
                    }
                }
            }
        }
        //17:43:00.482 [main] INFO com.hdu.Main - 第 2 次任务从任务队列里面移除
        //17:43:00.482 [pool-1-thread-1] INFO com.hdu.Task - 第 1 次任务被打断了
        //17:43:00.484 [main] INFO com.hdu.Main - 第 3 次任务从任务队列里面移除
        //17:43:00.484 [main] INFO com.hdu.Main - 第 4 次任务从任务队列里面移除
        //17:43:00.484 [main] INFO com.hdu.Main - 第 5 次任务从任务队列里面移除
        //17:43:00.484 [main] INFO com.hdu.Main - 第 6 次任务被拒绝
        //17:43:00.484 [main] INFO com.hdu.Main - 第 7 次任务被拒绝
        //17:43:00.484 [main] INFO com.hdu.Main - 第 8 次任务被拒绝
        //17:43:00.484 [main] INFO com.hdu.Main - 第 9 次任务被拒绝
        //17:43:00.484 [main] INFO com.hdu.Main - 第 10 次任务被拒绝
    }

注意,要想被执行的任务被中断,你需要在任务执行逻辑中去相应中断,不然的话线程池永远也关不掉!

    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println("第 " + id + " 次开始执行");
        } catch (InterruptedException e) {
            // 相应中断
            // 重置打断标记
            Thread.currentThread().interrupt();
            if (Thread.currentThread().isInterrupted()) {
                log.info("第 {} 次任务被打断了", id);
            }
        }
    }

像上面这样,你必须对线程的中断行为做出响应,不然线程池永远也关不掉。

3. awaitTermination()

该方法用在 线程池关闭后,该方法会等待线程池里面所有的任务执行完成之后才停止阻塞。也就是说你可以等待线程池的所有任务执行完成之后完成一定逻辑。

    private static void shutdownAndTermination() {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            try {
                threadPool.execute(new Task(i + 1));
            } catch (Exception e) {
                log.info("第 {} 次任务被拒绝", i + 1);
            }
            if (i == 4) {
                threadPool.shutdown();
            }
        }

        try {
            boolean isStop = threadPool.awaitTermination(2, SECONDS);
            if (!isStop) {
                log.info("线程池超时");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        
        //17:46:23.414 [main] INFO com.hdu.Main - 第 6 次任务被拒绝
        //17:46:23.416 [main] INFO com.hdu.Main - 第 7 次任务被拒绝
        //17:46:23.416 [main] INFO com.hdu.Main - 第 8 次任务被拒绝
        //17:46:23.416 [main] INFO com.hdu.Main - 第 9 次任务被拒绝
        //17:46:23.416 [main] INFO com.hdu.Main - 第 10 次任务被拒绝
        //第 1 次开始执行
        //第 2 次开始执行
        //17:46:25.429 [main] INFO com.hdu.Main - 线程池超时
        //第 3 次开始执行
        //第 4 次开始执行
        //第 5 次开始执行
    }

2. 优雅关闭线程池

优雅关闭线程池的逻辑应该是

  1. 先调用 shutdown()逻辑
  2. 调用 awaitTermination(),等待线程池执行处理已经提交的任务。
  3. 如果超时,调用 shutDownNow()方法,强制中断所有任务
  4. 调用 awaitTermination()方法,等待那些无法响应中断的任务处理完成。
    public static void shutdownGracefully(ExecutorService threadPool) {
        if (threadPool != null && !threadPool.isShutdown()) {
            log.info("threadPool shutdown");
            threadPool.shutdown();
            try {
                if (!threadPool.awaitTermination(120, SECONDS)) {
                    List<Runnable> remainTasks = threadPool.shutdownNow();
                    for (Runnable task : remainTasks) {
                        if (task instanceof Task) {
                            log.info("第 {} 次任务从任务队列里面移除", ((Task) task).getId());
                            // 在这里可以对这些还未执行的任务做一些处理。
                        }
                    }
                    if (!threadPool.awaitTermination(60, SECONDS)) {
                        log.error("threadPool can not be close");
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }