1. 概述
一个ExecutorService
对象在后台运行任务。对在其他线程上运行的任务进行单元测试具有挑战性。主线程必须等待任务结束才能验证其结果。
此外,解决这个问题的一个方法是使用Thread.sleep()
方法。这个方法会让主线程阻塞一段时间。然而,如果任务超过了sleep()
设置的时间,单元测试会在任务完成之前结束并失败。
在这篇教程中,我们将学习如何不使用Thread.sleep()
方法来单元测试ExecutorService
实例。
2. 创建Runnable
对象
在深入测试之前,让我们创建一个实现Runnable
接口的类:
public class MyRunnable implements Runnable {
Long result;
public Long getResult() {
return result;
}
public void setResult(Long result) {
this.result = result;
}
@Override
public void run() {
result = sum();
}
private Long sum() {
Long result = 0L;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
result += i;
}
return result;
}
}
MyRunnable
类执行一个耗时很长的计算,然后将计算结果设置为result
成员字段。因此,这将是我们将提交给执行器的任务。
3. 问题
通常,一个ExecutorService
对象在一个后台线程中运行任务。任务实现了Callable
或Runnable
接口。
如果主线程没有等待,它会在任务完成前终止。所以,测试总是失败。
让我们创建一个单元测试来验证这个问题:
ExecutorService executorService = Executors.newSingleThreadExecutor();
MyRunnable r = new MyRunnable();
executorService.submit(r);
assertNull(r.getResult());
在这个测试中,我们首先创建了一个单线程的ExecutorService
实例。然后,我们创建并提交了任务。最后,我们断言了result
字段的值。
在运行时,断言发生在任务结束之前。所以,getResult()
返回null
。
4. 使用Future
类
Future
类表示后台任务的结果。而且,它可以阻止主线程直到任务完成。
让我们修改测试以使用submit()
方法返回的Future
对象:
Future<?> future = executorService.submit(r);
future.get();
assertEquals(2305843005992468481L, r.getResult());
这里,**Future
实例的get()
方法会阻塞直到任务结束**。
另外,如果任务是Callable
的实例,get()
方法可能会返回一个值。**如果任务是Runnable
的实例,get()
始终返回null
**。
现在运行测试比以前花费更长的时间。这是主线程正在等待任务完成的迹象。最后,测试成功了。
5. 关闭并等待
另一种选择是使用ExecutorService
类的shutdown()
和awaitTermination()
方法。
shutdown()
方法关闭执行器。执行器不再接受新任务,但不会杀死现有的任务。然而,它不会等待它们结束。
另一方面,我们可以使用**awaitTermination()
方法阻塞直到提交的所有任务结束**。此外,我们应该在方法上设置一个阻塞超时。超过超时意味着阻塞结束。
让我们调整之前的测试,使用这两个方法:
executorService.shutdown();
executorService.awaitTermination(10000, TimeUnit.SECONDS);
assertEquals(2305843005992468481L, r.getResult());
如您所见,我们在提交任务后关闭了执行器。接下来,我们调用awaitTermination()
来让线程阻塞直到任务完成。
此外,我们设置了最大超时时间为10000秒。因此,如果任务运行超过10000秒,即使任务还没有结束,方法也会解除阻塞。换句话说,如果我们设置一个小的超时值,awaitTermination()
会像Thread.sleep()
一样过早地解除阻塞。
确实,当我们运行测试时,它成功了。
6. 使用ThreadPoolExecutor
另一个选项是创建一个接受预定数量任务并在它们完成时阻塞的ExecutorService
对象。
一个简单的方法是扩展ThreadPoolExecutor
类:
public class MyThreadPoolExecutor extends ThreadPoolExecutor {
CountDownLatch doneSignal = null;
public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,
int jobsNumberToWaitFor) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
doneSignal = new CountDownLatch(jobsNumberToWaitFor);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
doneSignal.countDown();
}
public void waitDone() throws InterruptedException {
doneSignal.await();
}
}
在这里,我们创建了MyThreadPoolExecutor
类,它扩展了ThreadPoolExecutor
。在构造函数中,我们添加了jobsNumberToWaitFor
参数,这是我们计划提交的任务数量。
此外,类使用doneSignal
字段,它是CountDownLatch
类的一个实例。doneSignal
字段在构造函数中初始化为等待的任务数量。接下来,我们重写了afterExecute()
方法,将doneSignal
减一。当一个任务结束时,会调用afterExecute()
方法。
最后,我们有一个waitDone()
方法,它使用doneSignal
来阻塞直到所有任务结束。
此外,我们可以使用单元测试测试上述实现:
@Test
void whenUsingThreadPoolExecutor_thenTestSucceeds() throws InterruptedException {
MyThreadPoolExecutor threadPoolExecutor = new MyThreadPoolExecutor(3, 6, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), 20);
List<MyRunnable> runnables = new ArrayList<MyRunnable>();
for (int i = 0; i < 20; i++) {
MyRunnable r = new MyRunnable();
runnables.add(r);
threadPoolExecutor.submit(r);
}
threadPoolExecutor.waitDone();
for (int i = 0; i < 20; i++) {
assertEquals(2305843005992468481L, runnables.get(i).result);
}
}
在这个单元测试中,我们向执行器提交20个任务。然后,我们立即调用waitDone()
方法,阻塞直到20个任务完成。最后,我们断言每个任务的结果。
7. 总结
在这篇文章中,我们学习了如何不使用Thread.sleep()
方法来单元测试ExecutorService
实例。具体来说,我们探讨了三种方法:
- 获取
Future
对象并调用get()
方法 - 关闭执行器并等待运行中的任务完成
- 创建自定义
ExecutorService
如往常一样,我们的示例的完整源代码可以在GitHub上找到。