Why a parallelism ForkJoinPool double my exception

2019-02-10 19:17发布

问题:

assuming I have the code like as below:

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

there is no problem when using ForkJoinPool#commonPool, but when I using a parallelism ForkJoinPool it will double the IllegalStateException. for example:

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

Q1: why the parallelism ForkJoinPool double the Exception occurs in the Callable?

Q2: how to avoiding this strange behavior?

回答1:

The Fork/Join pool generally attempts to recreate the exception within the caller’s thread if the exception has been thrown in a worker thread and sets the original exception as its cause. This is what you perceived as “doubling”. When you look closer at the stack traces, you will notice the difference between these two exceptions.

The common pool is not different in that regard. But the common pool allows the caller thread to participate in the work when waiting for the final result. So when you change you code to

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

you will notice that it often happens that the caller thread is faster in calling get() and do work-stealing within that method than a worker thread can pick up the task. In other words, your supplier has been executed within the main/caller thread and in this case, the exception will not be recreated.

This feature can easily disabled by throwing an exception type which has no matching public constructor the F/J could use, like with this neat inner 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;
                }
            };
    });
}


回答2:

The ForkJoinPool creates ForkJoinTask instances to execute your submissions.

ForkJoinTask tries to provide an accurate stack trace when exceptions occur. Its javadoc states

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.

This is 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() {

In other words, it takes the IllegalStateException your code threw, finds a constructor of IllegalStateException that receives a Throwable, invokes that constructor with the original IllegalStateException as its argument, and returns the result (which is then rethrown within a ExecutionException).

Your stack trace now also contains the stack trace for the get call.

With ForkJoinPool as your ExecutorService, I don't believe you can avoid it, it's dependent on if the exception was not thrown by the current thread and the constructors available in the thrown exception type.