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

hol*_*ava 4 java exception fork-join java-8

假设我有如下代码:

Future<Object> executeBy(ExecutorService executor) {
    return executor.submit(() -> {
        throw new IllegalStateException();
    });
}
Run Code Online (Sandbox Code Playgroud)

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

executeBy(new ForkJoinPool(1)).get(); 
//                              ^--- double the IllegalStateException
Run Code Online (Sandbox Code Playgroud)

Q1:为什么并行ForkJoinPool一倍Exception发生在Callable

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

Hol*_*ger 11

如果在工作线程中抛出异常并将原始异常设置为其原因,则Fork/Join池通常会尝试在调用者的线程内重新创建异常.这就是你所认为的"倍增".当您仔细观察堆栈跟踪时,您会注意到这两个异常之间的区别.

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

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

你会注意到,通常会发生调用者线程在调用中更快get()并且在该方法中进行工作窃取而不是工作线程可以接收任务.换句话说,您的供应商已在主/调用者线程中执行,在这种情况下,将不会重新创建异常.

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

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;
                }
            };
    });
}
Run Code Online (Sandbox Code Playgroud)

  • 我添加了一种可能性. (2认同)
  • @Eugene:不,正如在[其他答案](/sf/answers/3129400501/)中详细阐述的那样,如果抛出异常,它是报告两个堆栈跟踪的F/J的一个特殊功能在工作线程中,通过在调用程序线程中创建异常的副本来实现.工作窃取发生在另一个场景中,其中发起线程提交任务,然后立即调用`get()`并从队列中抢回作业并自行执行,因此不涉及其他线程.然后,不会发生异常复制. (2认同)

Sot*_*lis 8

ForkJoinPool创建ForkJoinTask实例来执行你的意见.

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

Rethrown异常的行为方式与常规异常相同,但在可能的情况下,包含ex.printStackTrace()启动计算的线程以及实际遇到异常的线程的堆栈跟踪(如示例所示 ).最低限度只有后者.

这是执行此行为的注释private

/**
 * 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() {
Run Code Online (Sandbox Code Playgroud)

换句话说,它需要IllegalStateException你的代码抛出,找到一个IllegalStateException接收a 的构造函数Throwable,调用该构造函数并将原始IllegalStateException作为其参数,并返回结果(然后在a中重新抛出ExecutionException).

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

随着ForkJoinPool作为你的ExecutorService,我不相信你可以避开它,它是依赖于如果异常没有被当前线程抛出在抛出的异常类型和可用的构造函数.

  • @ holi-java使用不同的`ExecutorService`实现,或者从没有无参数构造函数的类或具有`Throwable`参数的构造函数抛出异常.此外,您可以随时打开包装. (3认同)