Home > Software design >  Throwing exception from CompletableFuture hangs get() and join() in Java 17
Throwing exception from CompletableFuture hangs get() and join() in Java 17

Time:11-26

After I have switched from Java 11 to Java 17 (OpenJDK installed from Ubuntu 20.04 repository), the following code doesn't work:

import java.util.Objects;
import java.util.concurrent.CompletableFuture;

import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.exception.ExceptionUtils;

public class TestClass {

    public static void main(String[] args) {
        CompletableFuture<String> streamFuture = CompletableFuture.supplyAsync(() -> {
            throw MyException.wrapIfNeeded(new Exception("MyException"));
        });
        String result = null;
        try {
            result = streamFuture.get();
        } catch (Exception e) {
            System.out.println("Exception: "   ExceptionUtils.getMessage(e));
        }
        System.out.println("Result: "   Objects.toString(result));
    }

    static class MyException extends RuntimeException {

        private static final long serialVersionUID = 3349188601484197015L;

        public MyException(Throwable cause) {
            super(cause == null ? null : cause.getMessage(), cause);
        }

        public static MyException wrapIfNeeded(Throwable e) {
            return e instanceof MyException ? (MyException) e : new MyException(e);
        }

        @Override
        public String toString() {
            return ReflectionToStringBuilder.toString(this);
        }
    }
}

There is a problem in streamFuture.get() - it hangs infinitely. I dug deeper and found that in java.util.concurrent.ForkJoinPool there is a method unmanagedBlock(ManagedBlocker blocker) which looks like

    /** ManagedBlock for external threads */
    private static void unmanagedBlock(ManagedBlocker blocker)
        throws InterruptedException {
        if (blocker == null) throw new NullPointerException();
        do {} while (!blocker.isReleasable() && !blocker.block());
    }

and the program hangs infinitely in the do-while loop.

EDIT: I found out the problem is cause by the toString() method added to my custom exception class. For some reason it started to be a problem after Java 11

CodePudding user response:

The problem is due to ReflectionToStringBuilder.toString use encapsulated java API.

We can try something like MyException.wrapIfNeeded(new NullPointerException("")).toString() And will see

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field static final long java.lang.RuntimeException.serialVersionUID accessible: module java.base does not "opens java.lang" to unnamed module @b4c966a

Why will this cause hang infinitely?

In CompletableFuture.AsyncSupply#run method

    static final class AsyncSupply<T> extends ForkJoinTask<Void>
        implements Runnable, AsynchronousCompletionTask {
    ...
        public void run() {
            CompletableFuture<T> d; Supplier<? extends T> f;
            if ((d = dep) != null && (f = fn) != null) {
                dep = null; fn = null;
                if (d.result == null) {
                    try {
                        d.completeValue(f.get());
                    } catch (Throwable ex) {
                        d.completeThrowable(ex);
                    }
                }
                d.postComplete();
            }
        }

Since MyException is thrown, d.completeThrowable(ex); is called, inside this method, it will wrap the exception to CompletionException, which will call the constructor of Throwable with MyException as cause.

    public Throwable(Throwable cause) {
        fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }

When cause.toString() is called, it will throw the exception mentioned in the beginning, so d.completeThrowable(ex); is not executed completely and hence the program will hang forever.

CodePudding user response:

I found out the problem is cause by the toString() method added to my custom exception class. For some reason it (or to be more precise: ReflectionToStringBuilder.toString(this);) started to be a problem after Java 11.

  • Related