面试官:说一说如何优雅的关闭线程池,我:shutdownNow,面试官:粗鲁!

简介: 【6月更文挑战第4天】面试官:说一说如何优雅的关闭线程池,我:shutdownNow,面试官:粗鲁!

写在开头


面试官:“小伙子,线程池使用过吗,来聊一聊它吧!”

我:“好的,然后巴拉巴拉一顿输出之前看过的build哥线程池十八问...”

面试官满意的点了点头,紧接着问道:“那你知道如何优雅的关闭线程池吗?”

我:“知道知道,直接调用shutdownNow()方法就好了呀!”

面试官脸色一变,微怒道:“粗鲁!你给我滚出去!!!”


优雅的关闭线程池

哈哈,上面的场景是build哥臆想出来的面试画面,我们现在步入正题,来看一看在线程池使用完成后如何优雅的关闭线程池。

在JDK 1.8 中,Java 并发工具包中 java.util.concurrent.ExecutorService 提供了 shutdown()、shutdownNow()这两种接口方法去关闭线程池,我们分别看一下。

shutdown()
public void shutdown() {
   
   
    final ReentrantLock mainLock = this.mainLock; // ThreadPoolExecutor的主锁
    mainLock.lock(); // 加锁以确保独占访问

    try {
   
   
        checkShutdownAccess(); // 检查是否有关闭的权限
        advanceRunState(SHUTDOWN); // 将执行器的状态更新为SHUTDOWN
        interruptIdleWorkers(); // 中断所有闲置的工作线程
        onShutdown(); // ScheduledThreadPoolExecutor中的挂钩方法,可供子类重写以进行额外操作
    } finally {
   
   
        mainLock.unlock(); // 无论try块如何退出都要释放锁
    }
    tryTerminate(); // 如果条件允许,尝试终止执行器
}

在shutdown的源码中,会启动一次顺序关闭,在这次关闭中,执行器不再接受新任务,但会继续处理队列中的已存在任务,当所有任务都完成后,线程池中的线程会逐渐退出。

我们写一个小的demo来使用shutdown():

public class TestService{
   
   
    public static void main(String[] args) {
   
   
        //创建固定 3 个线程的线程池,测试使用,工作中推荐ThreadPoolExecutor
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        //向线程池提交 10 个任务
        for (int i = 1; i <= 10; i++) {
   
   
            final int index = i;
            threadPool.submit(() -> {
   
   
                System.out.println("正在执行任务 " + index);
                //休眠 3 秒,模拟任务执行
                try {
   
   
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
   
   
                    e.printStackTrace();
                }
            });
        }

        //休眠 4 秒
        try {
   
   
            Thread.sleep(4000);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }

        //关闭线程池
        threadPool.shutdown();
    }
}

在这段测试代码中,我们构造了一个包含固定3线程数的线程池,循环提交10个任务,每个任务休眠3秒,但主程序休眠4秒后,会掉用shutdown方法,理论上,在第二个时间循环中,线程池被停止,所以最多执行完6个任务,但从输出中,我们丝毫感受不好线程何时被停止了。
输出:

正在执行任务 1
正在执行任务 3
正在执行任务 2
正在执行任务 4
正在执行任务 5
正在执行任务 6
正在执行任务 7
正在执行任务 8
正在执行任务 9
正在执行任务 10
shutdownNow()
/**
 * 尝试停止所有正在执行的任务,停止处理等待的任务,
 * 并返回等待处理的任务列表。
 *
 * @return 从未开始执行的任务列表
 */
public List<Runnable> shutdownNow() {
   
   
    List<Runnable> tasks; // 用于存储未执行的任务的列表
    final ReentrantLock mainLock = this.mainLock; // ThreadPoolExecutor的主锁
    mainLock.lock(); // 加锁以确保独占访问
    try {
   
   
        checkShutdownAccess(); // 检查是否有关闭的权限
        advanceRunState(STOP); // 将执行器的状态更新为STOP
        interruptWorkers(); // 中断所有工作线程
        tasks = drainQueue(); // 清空队列并将结果放入任务列表中
    } finally {
   
   
        mainLock.unlock(); // 无论try块如何退出都要释放锁
    }
    tryTerminate(); // 如果条件允许,尝试终止执行器

    return tasks; // 返回队列中未被执行的任务列表
}

与shutdown不同的是shutdownNow会尝试终止所有的正在执行的任务,清空队列,停止失败会抛出异常,并且返回未被执行的任务列表。

由于shutdownNow会有返回值,所以我们将上面的测试案例稍作改动后输出结果为:

image.png

这种会在控制台抛出异常的方式,同样也不优雅,所以我们继续往下看!

shutdown()+awaitTermination(long timeout, TimeUnit unit)

awaitTermination(long timeout, TimeUnit unit)是可以允许我们在调用shutdown方法后,再设置一个等待时间,如设置为5秒,则表示shutdown后5秒内线程池彻底终止,返回true,否则返回false;

这种方式里,我们将shutdown()结合awaitTermination(long timeout, TimeUnit unit)方法去使用,注意在调用 awaitTermination() 方法时,应该设置合理的超时时间,以避免程序长时间阻塞而导致性能问题,而且由于这个方法在超时后也会抛出异常,因此,我们在使用的时候要捕获并处理异常!

public class TestService{
   
   
    public static void main(String[] args) {
   
   
        //创建固定 3 个线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        //向线程池提交 10 个任务
        for (int i = 1; i <= 10; i++) {
   
   
            final int index = i;
            threadPool.submit(() -> {
   
   
                System.out.println("正在执行任务 " + index);
                //休眠 3 秒
                try {
   
   
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
   
   
                    e.printStackTrace();
                }
            });
        }
        //关闭线程池,设置等待超时时间 3 秒
        System.out.println("设置线程池关闭,等待 3 秒...");
        threadPool.shutdown();
        try {
   
   
            boolean isTermination = threadPool.awaitTermination(3, TimeUnit.SECONDS);
            System.out.println(isTermination ? "线程池已停止" : "线程池未停止");
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }

        //再等待超时时间 20 秒
        System.out.println("再等待 20 秒...");
        try {
   
   
            boolean isTermination = threadPool.awaitTermination(20, TimeUnit.SECONDS);
            System.out.println(isTermination ? "线程池已停止" : "线程池仍未停止,请检查!");
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
    }
}

输出:

设置线程池关闭,等待 3...
正在执行任务 1
正在执行任务 2
正在执行任务 3
正在执行任务 4
正在执行任务 5
线程池未停止
再等待 20...
正在执行任务 6
正在执行任务 7
正在执行任务 8
正在执行任务 9
正在执行任务 10
线程池已停止

从输出中我们可以看到,通过将两种方法结合使用,我们监控了整个线程池关闭的全流程,实现了优雅的关闭!

目录
相关文章
|
3月前
|
存储 安全 Java
【Java集合类面试二十五】、有哪些线程安全的List?
线程安全的List包括Vector、Collections.SynchronizedList和CopyOnWriteArrayList,其中CopyOnWriteArrayList通过复制底层数组实现写操作,提供了最优的线程安全性能。
|
3月前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
2月前
|
消息中间件 前端开发 NoSQL
面试官:线程池遇到未处理的异常会崩溃吗?
面试官:线程池遇到未处理的异常会崩溃吗?
76 3
面试官:线程池遇到未处理的异常会崩溃吗?
|
2月前
|
消息中间件 存储 前端开发
面试官:说说停止线程池的执行流程?
面试官:说说停止线程池的执行流程?
51 2
面试官:说说停止线程池的执行流程?
|
2月前
|
消息中间件 前端开发 NoSQL
面试官:如何实现线程池任务编排?
面试官:如何实现线程池任务编排?
33 1
面试官:如何实现线程池任务编排?
|
3月前
|
安全 Java
【Java集合类面试十三】、HashMap如何实现线程安全?
实现HashMap线程安全的方法包括使用Hashtable类、ConcurrentHashMap,或通过Collections工具类将HashMap包装成线程安全的Map。
【多线程面试题 一】、 创建线程有哪几种方式?
创建线程的三种方式包括继承Thread类、实现Runnable接口和实现Callable接口,其中Runnable和Callable接口方式更受推荐,因为它们允许多重继承并更好地体现面向对象思想。
|
3月前
|
Java
【Java集合类面试十二】、HashMap为什么线程不安全?
HashMap在并发环境下执行put操作可能导致循环链表的形成,进而引起死循环,因而它是线程不安全的。
|
3月前
|
Java 调度
【多线程面试题 五】、 介绍一下线程的生命周期
线程的生命周期包括新建、就绪、运行、阻塞和死亡状态,线程状态会根据线程的执行情况在这些状态之间转换。
【多线程面试题 五】、 介绍一下线程的生命周期