调用ExecutorService的shutdown()方法的原因

112

我在过去几个小时里阅读了很多关于这个问题的内容,但我仍然无法找到任何一个(有效的)理由来调用shutdown()方法关闭ExecutorService,除非我们有一个存储着许多不常使用的执行器服务的庞大应用程序。

从我所了解的情况来看,shutdown()方法只会做与普通线程完成后一样的事情。当普通线程完成Runnable(或Callable)的run方法后,它将被传递给垃圾回收进行回收。对于执行器服务,线程将只是被暂时挂起,而不会被标记为可回收的垃圾。因此需要使用shutdown()方法。

好了,回到我的问题上来。有没有任何理由经常调用ExecutorServiceshutdown()方法,甚至是在提交任务后立即调用?我想排除某些情况下调用awaitTermination()的情况,因为这是验证过的。一旦我们这样做了,就必须重新创建一个新的ExecutorService,以实现同样的功能。难道ExecutorService的整个理念不是要重复使用线程吗?那么为什么要这么快销毁它呢?

难道不是一个合理的方法就是简单地创建ExecutorService(或根据需要创建多个),然后在应用程序运行时传递任务,最后在应用程序退出或其他重要阶段关闭这些执行器吗?

我希望得到一些经验丰富的编码人员对此问题的答案,他们经常使用ExecutorService编写异步代码。

第二个较小的问题涉及到安卓平台。如果你们中的一些人说每次都关闭执行器并不是最好的想法,那么在安卓上处理这些关闭(具体来说是在应用程序生命周期的不同事件中执行它们)会怎样呢?

由于CommonsWare的评论,我使这篇文章保持中立。我真的不想为此争论到底,似乎它正在朝着那个方向发展。我只对从有经验的开发人员那里学习我在这里问的东西感兴趣,如果他们愿意分享他们的经验的话。谢谢。


4
我经常看到一些示例代码,其中在提交或执行任务后总是立即调用shutdown()方法。个人而言,我从未见过这样做的“示例代码”。如果我们不知道您正在检查哪些“示例代码”,则可能是您误解了某些内容,我们只能向您指出这一点。 - CommonsWare
9
你好,CommonsWare。首先,我感觉你对我有点过于激烈的口气(或者看起来是这样),但我认为这里并没有证明这一点。我并不是试图贬低人们。至于你引用的内容,我主要是在谈论《Java编程思想》第四版中的多任务部分。你可以在Bruce Eckel的例子中找到很多这样的实例。它们大多数都很简单,但是Bruce给我的印象是经常使用shutdown。无论如何,你关注的那些内容并不是我帖子的主要部分。我删除了那些部分,因为我真的不想争论这个问题。 - Lucas
2
嗨,@CommonsWare,在Bruce Eckel的《Thinking in Java》第四版中的并发/Executor页面804中,他总是在简单应用程序中提交或执行任务后立即使用shutdown()方法来说明Executor的工作原理,正如Lucas所说。 - Error
2
我知道这是一篇旧帖子,但我认为楼主的问题仍然存在且有效。我也遇到了许多示例代码,在其中“在execute()之后有一个shutdown()调用”。http://tutorials.jenkov.com/java-util-concurrent/executorservice.html(当您谷歌“java executorservice example”时出现的第一个教程) - baekacaek
谢谢,我也有同样的问题,这些“示例代码”引起了我的注意。 - Gregordy
我的应用程序在我调用Executors.newSingleThreadScheduledExecutor()返回的执行器服务上调用shutdown()之前不会终止。 - Joe Lapp
6个回答

68

shutdown()方法有一个作用:防止客户端向执行器服务发送更多的工作请求。这意味着所有现有任务仍将运行直到完成,除非采取其他措施。即使是定时任务也是如此,例如对于ScheduledExecutorService:新的计划任务实例将不会运行。它还释放了任何后台线程资源。 这在各种情况下都可能很有用。

假设您有一个控制台应用程序,其中运行着N个任务的执行器服务。 如果用户按CTRL-C,您希望应用程序终止,可能会顺利地终止。 什么是顺利? 也许您希望您的应用程序不能向执行器服务提交更多任务,同时您希望等待现有的N个任务完成。 您可以使用关闭挂钩作为最后一道保险来实现这一点:

final ExecutorService service = ... // get it somewhere

Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Performing some shutdown cleanup...");
        service.shutdown();
        while (true) {
            try {
                System.out.println("Waiting for the service to terminate...");
                if (service.awaitTermination(5, TimeUnit.SECONDS)) {
                    break;
                }
            } catch (InterruptedException e) {
            }
        }
        System.out.println("Done cleaning");
    }
}));

这个钩子将关闭服务,防止应用程序提交新任务,并等待所有现有任务完成后关闭JVM。await termination会阻塞5秒钟,并在服务关闭时返回true。这样做是为了确保服务最终会关闭。每次都会忽略InterruptedException。这是关闭在应用程序中重复使用的执行器服务的最佳方法。

这段代码并不完美。除非您绝对确定您的任务最终会终止,否则您可能需要等待一个给定的超时时间,然后只需退出并放弃正在运行的线程。在这种情况下,在超时后调用shutdownNow()也是有意义的,以最后一次尝试中断运行的线程(shutdownNow()还将提供等待运行的任务列表)。如果您的任务设计响应中断,则可以正常工作。

另一个有趣的情况是当您拥有执行周期性任务的ScheduledExecutorService时。停止周期性任务链的唯一方法是调用shutdown()

编辑:我想补充说明的是,在一般情况下,我不建议像上面展示的那样使用关闭钩子:它可能存在错误,并且只应作为最后的选择。此外,如果您注册了许多关闭钩子,则它们运行的顺序是未定义的,这可能是不可取的。我宁愿应用程序在InterruptedException上显式调用shutdown()


1
抱歉Giovanni,回复晚了,顺便感谢你。是的,我知道Executor的工作原理,这也是我在问题中试图解释的。关闭操作确实会像你说的那样,它还允许垃圾收集器收集那些已经死亡的线程,并最终收集ExecutorService实例。我的问题很具体。是否有任何理由在提交/执行ExecutorService上的任何内容后立即调用“shutdown()”?问题的第二部分严格涉及Android架构。如果前面的答案是否定的,那么在整个生命周期中什么时候调用关闭操作? - Lucas
5
不必一直调用shutdown(),事实上这可能是完全错误的做法,因为它会阻止您再次重用执行器服务。在服务生命周期的末尾调用它的原因是使线程最终可以进行垃圾回收,正如您所注意到的那样。如果不这样做,即使它们处于空闲状态,这些线程也会使JVM保持活动状态。 - Giovanni Botta
没有必要一直调用shutdown()。事实上,这可能是完全错误的做法,因为它会阻止您再次重用执行器服务。这正是我的推理和原始问题的困境。因此,重申一下问题:在Android生命周期中何时应该关闭我的ExecutiveService? - Lucas
2
我没有安卓开发经验,但我猜想你应该在应用程序关闭时将其关闭,以允许JVM最终退出。 - Giovanni Botta
3
我了解了。我建议您使用“缓存线程池”(cached thread pool),并且不要在它上面调用 shutdown() 方法,这样可以避免不必要的资源浪费。如果应用程序关闭,池中的线程最终会被垃圾回收(默认情况下,当它们空闲60秒后)。请注意,如果您想限制线程池大小或希望有不同的线程寿命,您可以直接创建一个ThreadPoolExecutor - Giovanni Botta
显示剩余2条评论

15

ExecutorService不就是为了重复使用线程吗?那么为什么要这么快地销毁它呢?

是的。你不应该频繁地销毁和重新创建ExecutorService。在需要时(通常是在启动时)初始化ExecutorService并保持其处于活动状态,直到你完成工作。

是不是只需简单地创建ExecutorService(或者根据需要创建几个),然后在应用程序运行时将任务传递给它们,然后在应用程序退出或其他重要阶段关闭这些执行器是一种合理的方法?

是的。在重要阶段如应用程序退出等时关闭ExecutorService是合理的。

第二个问题,涉及 Android 平台,稍小一点。如果有人说每次关闭执行器都不是最好的选择,并且您正在编写 Android 应用程序,您能告诉我如何处理这些关闭吗(具体而言,当我们处理应用程序生命周期的不同事件时,如何执行它们)?

假设ExecutorService在您的应用程序中共享并跨不同活动使用。每个活动将在不同的时间间隔内暂停/恢复,仍需要一个ExecutorService

不要在活动生命周期方法中管理ExecutorService的状态,而是将 ExecutorService 管理(创建/关闭)移至您的自定义服务

在 Service 中的 onCreate() 创建 ExecutorService,在onDestroy()中正确地关闭它。

关闭ExecutorService的推荐方式:

如何正确关闭Java ExecutorService


我认为这个生命周期依赖问题是相对的,因为如果某些东西仍然向您发送可运行命令,并且如果您的执行者被生命周期终止的.shutdown(),则整个类仍需要从发送命令的生产者中取消引用(如果不这样做,则会泄漏)。这意味着:如果整个LifeCycle系统正确完成,并且每个Consumer都从其Producer正确取消引用,则每个.shutdown()都将是多余的,因为执行者的整个封闭体仍然符合垃圾收集的条件。 - Delark
如果选择的阶段是onStart()用于创建+onStop()用于关闭,那么这将完全改变。 - Delark
1
“您不应频繁销毁和重新创建ExecutorService。”尽管这是目前的真相(在Java 17中),但我想指出,如果Project Loom成功并引入基于虚拟线程的执行器服务,则此类建议将*不再适用于Java。在Loom下,我们将经常启动和关闭虚拟线程执行器服务。可能有其他原因限制我们使用后台虚拟线程,但保守地使用支持线程池将不再是其中之一的原因。 - Basil Bourque

7

一旦不再需要ExecutorService,应该关闭它以释放系统资源并允许优雅的应用程序关闭。因为ExecutorService中的线程可能是非守护线程,它们可能会阻止正常的应用程序终止。换句话说,在完成其主要方法后,您的应用程序仍在运行。

参考书目

第14章 第814页


1

调用shutdown()关闭ExecutorService的原因

今天我遇到了这样一种情况:在启动一系列任务之前,我必须等待机器准备好。

我向这台机器发出REST调用,如果我没有收到503(服务器不可用)的响应,则表示该机器已准备好处理我的请求。因此,我会等待第一个REST调用返回200(成功)。

有多种方法可以实现这一点,我使用ExecutorService创建一个线程,并安排它在每X秒后运行。因此,我需要在某个条件下停止这个线程,看看这个...

final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
    Runnable task = () -> {
        try {
            int statusCode = restHelper.firstRESTCall();

            if (statusCode == 200) {
                executor.shutdown();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    };

    int retryAfter = 60;
    executor.scheduleAtFixedRate(task, 0, retryAfter, TimeUnit.SECONDS);

第二个次要问题,与安卓平台有关。

如果您能提供更多上下文信息,也许我可以回答这个问题!从我在安卓开发方面的经验来看,很少需要使用线程。您是在开发需要性能线程的游戏或应用程序吗?如果不是,在安卓中,您有其他解决问题的方法,比如我上面解释的场景。您可以使用TimerTask、AsyncTask、Handlers或Loaders等方式,具体取决于上下文。这是因为如果UIThread等待时间过长,您知道会发生什么:/


0

即使是针对计划项目,例如ScheduledExecutorService,这也是真实的:新的预定任务实例将不会运行。

假设您有一个控制台应用程序,其中运行着N个任务的代理服务。

这意味着什么?也许您希望您的应用程序无法向代理服务提交更多任务,同时您需要等待当前的N个任务完成。

除非您完全确定您的任务最终会完成,否则您可能需要等待一段时间,然后退出,留下正在运行的线程。

如果您的操作旨在响应中断,则可以正常工作。

另一个有趣的情况是,当您拥有一个ScheduledExecutorService执行操作时。

停止操作链的唯一方法是调用shutdown()。


0
最近我在代码中发现,如果在完成一些并行工作后没有使用shutdown(),即使并行工作已经完成,线程也不会自动关闭。这会创建许多未使用的线程,最终由于内存不足问题导致代码崩溃。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接