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

jba*_*rez 4 java multithreading executorservice java-8

我想使用Executor接口(使用Callable)来启动一个Thread(我们称之为可调用的Thread),它将执行使用阻塞方法的工作.这意味着当主线程调用Future.cancel(true)(调用Thread.interrupt())时,可调用线程可以抛出InterruptedException.

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

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

以下JUnit 5代码段说明了该问题.如果主线程没有在cancel()get()调用之间休眠,我们可以轻松地重现它.如果我们睡了一会儿但是还不够,我们可以看到可调用线程做了一半的取消工作.如果我们足够睡眠,可调用线程正确完成其取消工作.

注1:我检查了可调用线程的中断状态:它正确设置一次,只能按预期设置一次.

注2:在中断后(当传入取消代码时)逐步调试我的可调用线程时,我在进入阻塞方法的几个步骤后"松散"它(似乎没有抛出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();
    }
Run Code Online (Sandbox Code Playgroud)

Hol*_*ger 5

当你打电话get()取消时Future,你会得到一个CancellationException,因此不会等待Callable代码执行清理.然后,您刚刚返回,并且当确定测试已完成时,被观察到的线程被杀死似乎是JUnit清理的一部分.

为了等待完全清理,请更改最后一行

executorService.shutdown();
Run Code Online (Sandbox Code Playgroud)

executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.DAYS);
Run Code Online (Sandbox Code Playgroud)

请注意,在方法的throws子句中声明意外异常更简单,而不是使用catch子句调用来混乱测试代码Assertions.fail.无论如何,JUnit都会报告失败等异常情况.

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

可能值得将ExecutorService管理放入@Before/ @After或甚至@BeforeClass/ @AfterClass方法,以使测试方法不受此限制,专注于实际测试.¹


¹这些是JUnit 4的名字.IIRC,JUnit 5的名字就像是@BeforeEach/ @AfterEachresp.@BeforeAll/@AfterAll