前言
博主以前面试的时候,真真切切的被问过这样一道题:
如何获取子线程的执行结果?
总所周知,在单线程情况下,想获取线程执行结果很简单,只需要写类似的代码即可:
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
类总共有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) {......}
显然实例中调用的是第一个,也就是说FutureTask
是Runnable
接口的实现类。是FutureTask
把Callable
接口的子类转换成了Runnable
接口的子类。
实现
先整体上看下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
接口的统一,FutureTask
和Future
接口。相信看完本篇之后,聪明的同学已经能把它应用到线程池了。
来源:CSDN
作者:Sicimike
链接:https://blog.csdn.net/Baisitao_/article/details/103882890