为什么并行 ForkJoinPool 加倍我的异常?

Why a parallelism ForkJoinPool double my exception?

假设我有如下代码:

Future<Object> executeBy(ExecutorService executor) {
    return executor.submit(() -> {
        throw new IllegalStateException();
    });
}

使用 ForkJoinPool#commonPool 时没有问题,但是当我使用并行度 ForkJoinPool 时,它会使 IllegalStateException 加倍。例如:

executeBy(new ForkJoinPool(1)).get(); 
//                              ^--- double the IllegalStateException

Q1: 为什么并行度ForkJoinPoolException的两倍出现在Callable

Q2: 如何避免这种奇怪的行为?

ForkJoinPool 创建 ForkJoinTask 个实例来执行您的提交。

ForkJoinTask 尝试在发生异常时提供准确的堆栈跟踪。它的 javadoc 表示

Rethrown exceptions behave in the same way as regular exceptions, but, when possible, contain stack traces (as displayed for example using ex.printStackTrace()) of both the thread that initiated the computation as well as the thread actually encountering the exception; minimally only the latter.

这是the comment in the private implementation of this behavior

/**
 * Returns a rethrowable exception for the given task, if
 * available. To provide accurate stack traces, if the exception
 * was not thrown by the current thread, we try to create a new
 * exception of the same type as the one thrown, but with the
 * recorded exception as its cause. If there is no such
 * constructor, we instead try to use a no-arg constructor,
 * followed by initCause, to the same effect. If none of these
 * apply, or any fail due to other exceptions, we return the
 * recorded exception, which is still correct, although it may
 * contain a misleading stack trace.
 *
 * @return the exception, or null if none
 */
private Throwable getThrowableException() {

换句话说,它需要 IllegalStateException 你的代码,找到接收 ThrowableIllegalStateException 的构造函数,用原始 IllegalStateException 调用该构造函数作为其参数,returns 结果(然后在 ExecutionException 中重新抛出)。

您的堆栈跟踪现在还包含 get 调用的堆栈跟踪。

使用ForkJoinPool作为你的ExecutorService,我不相信你可以避免它,这取决于如果当前线程没有抛出异常 和抛出的异常类型中可用的构造函数。

如果在工作线程中抛出了异常,Fork/Join 池通常会尝试在调用者的线程中重新创建异常,并将原始异常设置为其原因。这就是您认为的“加倍”。当您仔细查看堆栈跟踪时,您会注意到这两个异常之间的区别。

公共池在这方面没有什么不同。但是公共池允许调用者线程在等待最终结果时参与工作。因此,当您将代码更改为

static Future<Object> executeBy(ExecutorService executor) {
    return executor.submit(() -> {
        throw new IllegalStateException(Thread.currentThread().toString());
    });
}

您会注意到,调用者线程调用 get() 并在该方法中窃取工作的速度通常比工作线程可以接管任务的速度更快。换句话说,您的供应商已在 main/caller 线程内执行,在这种情况下,不会重新创建异常。

可以通过抛出一个异常类型来轻松禁用此功能,该异常类型没有 F/J 可以使用的匹配 public 构造函数,就像这个整洁的内部 class:

static Future<Object> executeBy(ExecutorService executor) {
    return executor.submit(() -> {
        throw new IllegalStateException() {
                @Override
                public String toString() {
                    String s = getClass().getSuperclass().getName();
                    String message = getLocalizedMessage();
                    return message!=null? s+": "+message: s;
                }
            };
    });
}