如何获取子线程的执行结果

不想你离开。 提交于 2020-01-09 16:43:29

前言

博主以前面试的时候,真真切切的被问过这样一道题:

如何获取子线程的执行结果?

总所周知,在单线程情况下,想获取线程执行结果很简单,只需要写类似的代码即可:

Object result = xxx.getXxx();

但是在多线程的情况下,想要获取子线程的执行结果,恐怕就没这么简单了。

剑走偏锋

我们都知道开启一个新线程的方式有两种:继承Thread类、实现Runnable接口。可是这两种方式都没有返回值,相信这也难不倒聪明的同学,可以把代码写成这样:

public class ThreadResultDemo {

    private volatile static int result = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            System.out.println("Hello Sicimike.");
            // 要返回的结果
            result = 1;
        });
        thread.start();
        thread.join();
        System.out.println("子线程返回的结果:" + result);
    }
}

执行结果:

Hello Sicimike.
子线程返回的结果:1

可以看到主线程成功的拿到了子线程的执行结果。
机智如我
类似的方法还有很多,不一一介绍了。功能虽然实现了,但是看起来总是不够优雅,还要考虑线程安全,并且代码不易维护。

Callable

JDK1.5开始,提供了一个能返回线程执行结果的接口Callable。接口定义非常简单,只有一个抽象方法

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     * 计算得到结果,如果无法计算就抛出异常
     */
    V call() throws Exception;
}

毫无疑问,需要定义子类去实现Callable接口:

public class CallableTask implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(1000);
        System.out.println("Hello Sicimike.");
        return 1;
    }
}

接下来就需要把这个task放到Thread类中去执行了。但是看下一下Thread
Thread类构造方法
Thread类总共有9个构造方法,但是没有一个构造方法能够接受传入Callable接口的子类。这样不就无解了吗?

想要子线程有返回值,就得实现Callable接口,实现了Callable接口就不能传递给Thread类。

这时我们需要借助JDK(JUC包中)提供的另一个类FutureTask

FutureTask

先看一下FutureTask如何解决这个问题:

/**
 * @author sicimike
 */
public class ThreadResultDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // Callable接口任务(前文实现了Callable接口)
        CallableTask task = new CallableTask();
        // 包装成FutureTask类
        FutureTask<Integer> futureTask = new FutureTask<>(task);
        new Thread(futureTask).start();
        // 阻塞的方式获取线程执行结果
        System.out.println(futureTask.get());
    }
}

执行结果

Hello Sicimike.
1

纵观Thread类的9个构造方法,只传入一个参数的构造方法有2个:

public Thread(Runnable target) {......}

public Thread(String name) {......}

显然实例中调用的是第一个,也就是说FutureTaskRunnable接口的实现类。是FutureTaskCallable接口的子类转换成了Runnable接口的子类。

实现

先整体上看下FutureTask类的体系结构
FutureTask类体系
正如我们所料,FutureTask实现了Runnable接口。

接下来再具体看看FutureTask的构造方法

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

通过这个构造方法,就可以在FutureTask中传入Callable实现类,而FutureTask又实现了Runnable接口,这样就完成了Callable接口到Runnable接口的转换。

除此之外,FutureTask还有一个构造方法

public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

该构造方法可以把Runnable接口转换成Callable接口,继续跟踪callable方法的实现

/**
 * java.util.concurrent.Executors.java
 */
public static <T> Callable<T> callable(Runnable task, T result) {
    if (task == null)
        throw new NullPointerException();
    return new RunnableAdapter<T>(task, result);
}

static final class RunnableAdapter<T> implements Callable<T> {
    final Runnable task;
    final T result;
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        task.run();
        return result;
    }
}

RunnableAdapter类通过实现Callable接口的方式,实现call方法,在call方法里调用Runnable接口实现类的run方法。相当于把一个Runnable接口适配成了Callable接口。

我们都知道有一种套路叫设计模式,其中有一种叫适配器模式

在设计模式中,适配器模式(英语:adapter pattern)有时候也称包装样式或者包装(wrapper)。将一个类的接口转接成用户所期待的。一个适配使得因接口不兼容而不能在一起工作的类能在一起工作,做法是将类自己的接口包裹在一个已存在的类中。

适配器模式通俗点讲就是:利用一个适配器,把一个不适合用户的接口适配成适合用户的接口,具体做法是将接口包裹在一个类中。

自此,Runnable接口和Callable接口实现了统一。

我想,这也是Oracle不把实现Callable接口称为第三种创建子线程的方法的原因吧。

根据前文FutureTask的类结构体系图可以看到,FutureTask除了实现Runnable接口外,还实现了Future接口。Future接口也正是FutureTask能获取子线程执行结果的原因。

Future

Future作为FutureTask的顶层接口,定义了五个管理线程任务的方法:

public interface Future<V> {

    /**
     * Attempts to cancel execution of this task.  This attempt will
     * fail if the task has already completed, has already been cancelled,
     * or could not be cancelled for some other reason. If successful,
     * and this task has not started when {@code cancel} is called,
     * this task should never run.  If the task has already started,
     * then the {@code mayInterruptIfRunning} parameter determines
     * whether the thread executing this task should be interrupted in
     * an attempt to stop the task.
     * 
     * 尝试取消执行此任务
     * 如果此任务已经完成、或者已经被取消、或者由于其他原因不能被取消,尝试会失败
     * 如果取消成功,并且任务尚未开始执行,该任务永远不会执行
     * 如果任务已经开始,由 mayInterruptIfRunning 参数决定是否中断正在执行该任务的线程,来停止该任务
     *
     * <p>After this method returns, subsequent calls to {@link #isDone} will
     * always return {@code true}.  Subsequent calls to {@link #isCancelled}
     * will always return {@code true} if this method returned {@code true}.
     * 
     * 该方法返回后,随后调用 isDone 方法总是返回true
     * 如果该方法返回true,随后调用 isCancelled 方法总是返回true
     */
    boolean cancel(boolean mayInterruptIfRunning);

    /**
     * Returns {@code true} if this task was cancelled before it completed
     * normally.
     * 
     * 如果此任务在正常执行完成之前被取消,该方法返回true
     */
    boolean isCancelled();

    /**
     * Returns {@code true} if this task completed.
     *
     * Completion may be due to normal termination, an exception, or
     * cancellation -- in all of these cases, this method will return
     * {@code true}.
     * 
     * 返回次任务是否完成(不管是正常执行完成,还是异常,还是被取消了)
     */
    boolean isDone();

    /**
     * Waits if necessary for the computation to complete, and then
     * retrieves its result.
     * 等待计算完成,并返回结果(阻塞)
     * 
     * @return the computed result
     * @throws CancellationException if the computation was cancelled
     * 如果计算被取消,抛出CancellationException异常
     * 
     * @throws ExecutionException if the computation threw an exception
     * 如果计算抛出异常,抛出ExecutionException异常
     * 
     * @throws InterruptedException if the current thread was interrupted
     * while waiting
     * 如果线程在等待过程中被中断,抛出InterruptedException异常
     */
    V get() throws InterruptedException, ExecutionException;

    /**
     * Waits if necessary for at most the given time for the computation
     * to complete, and then retrieves its result, if available.
     * 最多等待指定长度的时间,如果有结果就返回
     *
     * @throws CancellationException if the computation was cancelled
     * 如果计算被取消,抛出CancellationException异常
     * 
     * @throws ExecutionException if the computation threw an exception
     * 如果计算抛出异常,抛出ExecutionException异常
     * 
     * @throws InterruptedException if the current thread was interrupted
     * while waiting
     * 如果线程在等待过程中被中断,抛出InterruptedException异常
     * 
     * @throws TimeoutException if the wait timed out
     * 如果超时,抛出TimeoutException异常
     */
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

总结

本篇主要讲解了如何优雅的获取子线程的执行结果,Runnable接口和Callable接口的统一,FutureTaskFuture接口。相信看完本篇之后,聪明的同学已经能把它应用到线程池了。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!