【问题标题】:Why a parallelism ForkJoinPool double my exception?为什么并行性 ForkJoinPool 使我的异常加倍?
【发布时间】:2017-06-22 17:03:59
【问题描述】:

假设我有如下代码:

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:如何避免这种奇怪的行为?

【问题讨论】:

标签: java exception java-8 fork-join


【解决方案1】:

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

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

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

您会注意到,调用者线程调用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;
                }
            };
    });
}

【讨论】:

  • 嗨,@Holger。有没有更简单的方法来避免这种奇怪的行为?我认为唯一的方法是检查它是否是并行性ForkJoinPool,然后包装返回的Future
  • 我添加了一种可能性。
  • 我的意思是,除了看到一个显示 IllegalStateException 由 IllegalStateException 引起的堆栈之外……奇怪的行为到底是什么?在我看来,这是为了正确保留行号和堆栈的纯度。
  • @Holger 整洁的内部类很漂亮。我要保留在 ole 收藏夹中的东西
  • @Eugene: 不,正如other answer 中更详细地阐述的那样,如果在工作线程中抛出异常,它会报告两个堆栈跟踪,这是 F/J 的一个特殊功能,这是实现的通过在调用者线程中创建异常的副本。工作窃取发生在另一种情况下,启动线程提交任务,然后立即调用get()并从队列中窃取工作并自行执行,因此不涉及其他线程。然后,不会发生异常复制。
【解决方案2】:

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

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

重新抛出的异常与常规异常的行为方式相同,但是, 如果可能,包含堆栈跟踪(例如使用 ex.printStackTrace()) 的两个线程启动 计算以及实际遇到异常的线程; 至少只有后者。

这是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 作为其参数调用该构造函数,并返回结果(然后在 ExecutionException 内重新抛出)。

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

ForkJoinPool 作为你的ExecutorService,我不相信你可以避免它,这取决于当前线程是否抛出异常 以及抛出的可用构造函数异常类型。

【讨论】:

  • 谢谢先生。但我想避免这种奇怪的行为。
  • @holi-java 使用不同的ExecutorService 实现或从没有无参数构造函数或具有Throwable 参数的构造函数的类中抛出异常。此外,您可以随时打开包装。
猜你喜欢
  • 2011-08-04
  • 1970-01-01
  • 2016-08-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-01
  • 1970-01-01
相关资源
最近更新 更多