Por que um paralelismo ForkJoinPool dobrar minha exceção?

assumindo que eu tenho o código como abaixo:

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

não há problema ao usar o ForkJoinPool#commonPool , mas quando eu uso um paralelismo ForkJoinPool ele irá dobrar o IllegalStateException . por exemplo:

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

Q1 : por que o paralelismo ForkJoinPool duplica a Exception ocorre no Callable ?

Q2 : como evitar esse comportamento estranho?

O fork / join pool geralmente tenta recriar a exceção dentro do thread do chamador se a exceção foi lançada em um thread de trabalho e define a exceção original como sua causa. Isso é o que você percebeu como “duplicação”. Quando você olha mais de perto os traços da pilha, você notará a diferença entre essas duas exceções.

A piscina comum não é diferente a esse respeito. Mas o conjunto comum permite que o encadeamento do chamador participe do trabalho ao aguardar o resultado final. Então, quando você muda seu código para

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

Você notará que muitas vezes acontece que o thread de chamador é mais rápido em chamar get() e fazer trabalho de roubo dentro desse método que um thread de trabalho pode pegar a tarefa. Em outras palavras, seu fornecedor foi executado dentro do encadeamento main / caller e, nesse caso, a exceção não será recriada.

Esse recurso pode ser facilmente desativado lançando um tipo de exceção que não possui um construtor público correspondente que o F / J possa usar, como com esta class interna pura:

 static Future 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; } }; }); } 

O ForkJoinPool cria instâncias ForkJoinTask para executar seus envios.

ForkJoinTask tenta fornecer um rastreamento de pilha preciso quando ocorrem exceções. Seus estados javadoc

As exceções de recuo se comportam da mesma maneira que as exceções regulares, mas, quando possível, contêm rastreamentos de pilha (como exibido, por exemplo, usando ex.printStackTrace() ) do encadeamento que iniciou o cálculo, bem como o encadeamento que realmente encontra a exceção; minimamente apenas o último.

Este é o comentário na implementação private deste comportamento

 /** * 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() { 

Em outras palavras, ele pega o IllegalStateException seu código lançou, localiza um construtor de IllegalStateException que recebe um Throwable , chama esse construtor com o IllegalStateException original como seu argumento e retorna o resultado (que é relançado novamente dentro de uma ExecutionException ).

Seu rastreamento de pilha agora também contém o rastreamento de pilha para a chamada get .

Com o ForkJoinPool como seu ExecutorService , eu não acredito que você possa evitá-lo, depende se a exceção não foi lançada pelo thread atual e os construtores disponíveis no tipo de exceção lançada.