为什么 JUnit 使用 CountDownLatch 来实现 FailOnTimeout

Why JUnit using CountDownLatch to implement FailOnTimeout

我刚开始阅读 JUnit 4.13 (https://github.com/junit-team/junit) 的代码,对 org.junit.internal.runners.statements.FailOnTimeout 的实现有点困惑:

@Override
public void evaluate() throws Throwable {
    CallableStatement callable = new CallableStatement();
    FutureTask<Throwable> task = new FutureTask<Throwable>(callable);
    ThreadGroup threadGroup = new ThreadGroup("FailOnTimeoutGroup");
    Thread thread = new Thread(threadGroup, task, "Time-limited test");
    thread.setDaemon(true);
    thread.start();
    callable.awaitStarted();
    Throwable throwable = getResult(task, thread);
    if (throwable != null) {
        throw throwable;
    }
}

/**
 * Wait for the test task, returning the exception thrown by the test if the
 * test failed, an exception indicating a timeout if the test timed out, or
 * {@code null} if the test passed.
 */
private Throwable getResult(FutureTask<Throwable> task, Thread thread) {
    try {
        if (timeout > 0) {
            return task.get(timeout, timeUnit);  // HERE limits the time
        } else {
            return task.get();
        }
    } catch (InterruptedException e) {
        return e; // caller will re-throw; no need to call Thread.interrupt()
    } catch (ExecutionException e) {
        // test failed; have caller re-throw the exception thrown by the test
        return e.getCause();
    } catch (TimeoutException e) {
        return createTimeoutException(thread);
    }
}

其中 CallableStatement 是:

private class CallableStatement implements Callable<Throwable> {
    private final CountDownLatch startLatch = new CountDownLatch(1);

    public Throwable call() throws Exception {
        try {
            startLatch.countDown();
            originalStatement.evaluate();  // HERE the test runs
        } catch (Exception e) {
            throw e;
        } catch (Throwable e) {
            return e;
        }
        return null;
    }

    public void awaitStarted() throws InterruptedException {
        startLatch.await();
    }
}

下面是我对代码的理解:

evaluate() 为测试方法启动一个新线程。 callable.awaitStarted()evaluate() 直到 startLatch.countDown(),然后 getResult() 次测试方法。

这是我的问题:

我对并发不是很熟悉。如果有人能解释这些或指出我的错误,我将不胜感激。谢谢。


关于第二个问题的更多解释:

我会将这两个线程表示为 "evaluate() thread" 和 "CallableStatement thread"。

我认为 "evaluate() thread" 在执行 callable.awaitStarted() 时被阻塞,直到 startLatch.countDown() 完成,但测试方法可能会在上下文切换回 [之前开始 运行 =59=]。 "evaluate() thread"再次唤醒后立即调用FutureTask.get(),会阻塞"evaluate() thread",但不能保证"CallableStatement thread"紧接着被唤醒。

所以,我认为测试方法开始的时刻与调用 task.get(timeout, timeUnit) 的时刻无关。如果有很多其他线程,它们之间的时间间隔可以忽略不计。

Why thread (in evaluate()) should be a daemon thread?

如果测试有任何问题,它允许 JVM 正确退出。另见 What is Daemon thread in Java?

Is CountDownLatch just used for blocking the getResult() until thread is running? Does it really work (I thought nothing can avert a context switch between callable.awaitStarted() and getResult())? Is there any "simpler" way to do this?

没有什么可以避免上下文切换,但如果 thread 已经启动并且 运行 它最终会得到一些 CPU 关注并且 originalStatement.evaluate() 将被执行。这样做的原因是可能没有可用的底层操作系统线程,然后测试的测试执行可能会失败,尽管测试本身是正确的。还有其他方法可以做到这一点,例如Semaphore,但是 CountDownLatch 非常有效,而且 便宜 原始。