从 CompletableFuture 抛出异常会导致 Java 17 中的 get() 和 join() 挂起

Chr*_*epa 6 java asynchronous freeze completable-future java-17

当我从 Java 11 切换到 Java 17(从 Ubuntu 20.04 存储库安装 OpenJDK)后,以下代码不起作用:

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

有一个问题streamFuture.get()- 它无限挂起。我深入挖掘,发现有java.util.concurrent.ForkJoinPool一种方法unmanagedBlock(ManagedBlocker blocker)看起来像

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

并且程序无限地挂在 do-while 循环中。

编辑:我发现问题是由toString()添加到我的自定义异常类中的方法引起的。由于某种原因,它在 Java 11 之后开始成为一个问题

sam*_*cde 8

问题是由于ReflectionToStringBuilder.toString使用了封装的java API。

我们可以尝试类似的方法MyException.wrapIfNeeded(new NullPointerException("")).toString() 并且会看到

线程“main”中的异常java.lang.reflect.InaccessibleObjectException:无法使字段静态最终长java.lang.RuntimeException.serialVersionUID可访问:模块java.base不会“打开java.lang”到未命名模块@b4c966a

为什么这个原因会无限挂起?

CompletableFuture.AsyncSupply#run方法中

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

由于在该方法内MyException抛出、d.completeThrowable(ex);调用,它将异常包装到,这将调用with的CompletionException构造函数。ThrowableMyExceptioncause

    public Throwable(Throwable cause) {
        fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }
Run Code Online (Sandbox Code Playgroud)

cause.toString()调用时,它会抛出开头提到的异常,因此d.completeThrowable(ex);没有完全执行,因此程序将永远挂起。


Chr*_*epa 1

我发现问题是由toString()添加到我的自定义异常类中的方法引起的。由于某种原因,它(或者更准确地说:)ReflectionToStringBuilder.toString(this);在 Java 11 之后开始成为一个问题。