我想使用 Executor 接口(使用 Callable )来启动 Thread (我们将其称为可调用线程),做使用阻塞方法的工作。 这意味着当主线程调用 Future.cancel(true)(调用 Thread.interrupt()时,可调用线程可以引发 InterruptedException )。 >)。
我还希望我的可调用线程在取消部分代码中使用其他阻止方法中断时正确终止。
在执行此操作时,我遇到以下行为:当我调用 Future.cancel(true)方法时,如果主线程立即等待,则会正确通知可调用线程中断 BUT。为了使用 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();
}
答案 0 :(得分:5)
在取消的get()
上调用Future
时,您会得到CancellationException
,因此不会等待Callable
的代码执行清理。然后,您只是返回而已,当JUnit确定测试已完成时,观察到的被杀死线程的行为似乎是JUnit清理的一部分。
为了等待完整清理,将最后一行更改为
executorService.shutdown();
到
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.DAYS);
请注意,在方法的throws
子句中声明意外的异常比在调用catch
的{{1}}子句中弄乱测试代码要容易得多。无论如何,JUnit都会报告诸如失败之类的异常。
然后,您可以删除整个Assertions.fail
代码。
可能值得将sleep
管理放入ExecutorService
/ @Before
甚至@After
/ @BeforeClass
方法中,以保持测试方法不受限制,着重于实际测试。¹
¹这些是JUnit 4名称。在IIRC中,JUnit 5的名称分别为@AfterClass
/ @BeforeEach
。 @AfterEach
/ @BeforeAll