yuanzhixiang's blog

yuanzhixiang

异步方法 execute 与 submit 该如何运用

241
2022-09-10

在 java 的世界里,用到的异步方法一般名叫 execute 和 submit,有些同学不太理解为什么有这两个方法,所以会出现哪个单词写的顺手就用哪个,但实际上这两个方法的使用需要符合其本身的设计哲学来使用,否则就可能会掉入一些陷阱。

一般而言,execute 方法不含返回值,submit 方法会有一个 future/promise 的返回值,这是这两个方法的不同点。execute 由于其不含返回值,所以如果运行的代码中出现异常,那么框架要么会打印日志,要么会提供扩展接口让开发者实现自己打日志的方法。而 submit 提供了返回值,所以从设计角度来看既然提供了返回值,那么就可以将异常等信息放入返回值中,让开发这自己处理,所以如果我们用了 submit 那么我们需要自己处理异常。下面来看看 jdk 中的 ThreadPoolExecute 和 Netty EventLoop 对 execute 和 submit 在异常处理上的实现。

// ThreadPoolExecute 的实现:

// execute 对异常的处理部分:
// java.lang.Thread.java
    
    /**
     * Dispatch an uncaught exception to the handler. This method is
     * intended to be called only by the JVM.
     */
    private void dispatchUncaughtException(Throwable e) {
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }
    
    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                // 最终会执行到这里
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }
    
// submit 对异常的处理部分:
// java.util.concurrent.FutureTask.java

    public void run() {
        // ...
        try {
            result = c.call();
            ran = true;
        } catch (Throwable ex) {
            result = null;
            ran = false;
            // 这里保存异常
            setException(ex);
        }
        // ...
    }

ThreadPoolExecute 的 execute 如果出现异常默认会去找线程中的默认异常处理器来处理,而 submit 仅仅是将其保存在 FutureTask 就结束了。

// Netty EventLoop 的实现:

// execute 对异常的处理部分:
// io.netty.util.concurrent.AbstractEventExecutor.java

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractEventExecutor.class);

    /**
     * Try to execute the given {@link Runnable} and just log if it throws a {@link Throwable}.
     */
    protected static void safeExecute(Runnable task) {
        try {
            runTask(task);
        } catch (Throwable t) {
            // execute 内部如果出现异常那么会打印
            logger.warn("A task raised an exception. Task: {}", task, t);
        }
    }

// submit 对异常的处理部分:
// io.netty.util.concurrent.PromiseTask.java

    @Override
    public void run() {
        try {
            if (setUncancellableInternal()) {
                V result = runTask();
                setSuccessInternal(result);
            }
        } catch (Throwable e) {
            setFailureInternal(e);
        }
    }

    protected final Promise<V> setFailureInternal(Throwable cause) {
        // 当前类是 Promise,所以这里将 cause 保存起来
        super.setFailure(cause);
        clearTaskAfterCompletion(true, FAILED);
        return this;
    }

Netty EventLoop 在 execute 的处理逻辑中直接加入了 logger,如果出现异常那么则打印,而对 submit 的处理则将其保存在 PromiseTask 中。

通过上述两个案例能对框架处理 execute 和 submit 产生一些体感,优秀的框架也符合这个设计原则。这也可以对我们开发者带来启示,如果我们设计的异步方法是没有返回值的,那么就要考虑是不是要在代码中处理好异常逻辑,如果方法返回了 Future 这类的返回值,那么就要考虑是不是将正常和异常的返回值都进行返回交给开发者处理。