Future.cancel() 后跟 Future.get() 杀死我的线程

Future.cancel() followed by Future.get() kills my thread

我想使用 Executor 接口(使用 Callable)来启动一个 Thread(让我们称它为可调用线程),它将完成使用阻塞方法的工作。 这意味着当主线程调用 Future.cancel(true)(调用 Thread.interrupt()).

我还希望我的可调用线程在代码的取消部分使用其他阻塞方法中断时正确终止。

在实现这个时,我遇到了以下行为:当我调用 Future.cancel(true) 方法时,可调用线程被正确通知中断 但是如果主线程立即使用 Future.get() 等待其终止,则可调用线程在调用时有点 killed任何阻塞方法.

以下 JUnit 5 片段说明了这个问题。 如果主线程在 cancel()get() 调用之间不休眠,我们可以很容易地重现它。 如果我们睡了一会儿但还不够,我们可以看到可调用线程正在执行一半的取消工作。 如果我们睡够了,可调用线程就会正确地完成它的取消工作。

注意 1:我检查了可调用线程的 interrupted 状态:正如预期的那样,它被正确设置一次且仅一次。

注2:在中断后(传入取消代码时)逐步调试我的可调用线程时,我"loose"它在进入a时经过几步阻塞方法(没有InterruptedException似乎被抛出)。

    @Test
    public void testCallable() {

        ExecutorService executorService = Executors.newSingleThreadExecutor();

        System.out.println("Main thread: Submitting callable...");
        final Future<Void> future = executorService.submit(() -> {

            boolean interrupted = Thread.interrupted();

            while (!interrupted) {
                System.out.println("Callable thread: working...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    System.out.println("Callable thread: Interrupted while sleeping, starting cancellation...");
                    Thread.currentThread().interrupt();
                }
                interrupted = Thread.interrupted();
            }

            final int steps = 5;
            for (int i=0; i<steps; ++i) {
                System.out.println(String.format("Callable thread: Cancelling (step %d/%d)...", i+1, steps));
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    Assertions.fail("Callable thread: Should not be interrupted!");
                }
            }

            return null;
        });

        final int mainThreadSleepBeforeCancelMs = 2000;
        System.out.println(String.format("Main thread: Callable submitted, sleeping %d ms...", mainThreadSleepBeforeCancelMs));

        try {
            Thread.sleep(mainThreadSleepBeforeCancelMs);
        } catch (InterruptedException e) {
            Assertions.fail("Main thread: interrupted while sleeping.");
        }

        System.out.println("Main thread: Cancelling callable...");
        future.cancel(true);
        System.out.println("Main thread: Cancelable just cancelled.");

        // Waiting "manually" helps to test error cases:
        // - Setting to 0 (no wait) will prevent the callable thread to correctly terminate;
        // - Setting to 500 will prevent the callable thread to correctly terminate (but some cancel process is done);
        // - Setting to 1500 will let the callable thread to correctly terminate.
        final int mainThreadSleepBeforeGetMs = 0;
        try {
            Thread.sleep(mainThreadSleepBeforeGetMs);
        } catch (InterruptedException e) {
            Assertions.fail("Main thread: interrupted while sleeping.");
        }

        System.out.println("Main thread: calling future.get()...");
        try {
            future.get();
        } catch (InterruptedException e) {
            System.out.println("Main thread: Future.get() interrupted: Error.");
        } catch (ExecutionException e) {
            System.out.println("Main thread: Future.get() threw an ExecutionException: Error.");
        } catch (CancellationException e) {
            System.out.println("Main thread: Future.get() threw an CancellationException: OK.");
        }

        executorService.shutdown();
    }

当您在取消的 Future 上调用 get() 时,您将获得 CancellationException,因此不会等待 Callable 的代码执行其清理.然后,您刚刚返回,当 JUnit 确定测试已完成时,观察到的线程被杀死的行为似乎是 JUnit 清理的一部分。

为了等待完全清理,将最后一行从

executorService.shutdown();

executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.DAYS);

请注意,在方法的 throws 子句中声明意外异常比使用调用 Assertions.failcatch 子句使测试代码混乱更简单。 JUnit无论如何都会将此类异常报告为失败。

然后,您可以删除整个 sleep 代码。

可能值得将 ExecutorService 管理放入 @Before/@After 甚至 @BeforeClass/@AfterClass 方法中,以保持测试方法免费其中,专注于实际测试。¹


¹ 这些是 JUnit 4 名称。 IIRC,JUnit 5 名称分别类似于 @BeforeEach/@AfterEach@BeforeAll/@AfterAll