码迷,mamicode.com
首页 > 编程语言 > 详细

不会用Java Future,我怀疑你泡茶没我快, 又是超长图文!!

时间:2020-07-31 01:16:19      阅读:87      评论:0      收藏:0      [点我收藏+]

标签:volatil   method   process   sub   工厂   相关   简单   rip   lambda   

你有一个思想,我有一个思想,我们交换后,一个人就有两个思想
If you can NOT explain it simply, you do NOT understand it well enough
现陆续将Demo代码和技术文章整理在一起 Github实践精选 ,方便大家阅读查看,本文同样收录在此,觉得不错,还请Star
技术图片
前言
创建线程有几种方式?这个问题的答案应该是可以脱口而出的吧

  • 继承 Thread 类
  • 实现 Runnable 接口
    但这两种方式创建的线程是属于”三无产品“:
  • 没有参数
  • 没有返回值
  • 没办法抛出异常

    class MyThread implements Runnable{
    @Override
    public void run() {
      log.info("my thread");
    }
    }
    Runnable 接口是 JDK1.0 的核心产物
    /**
    * @since   JDK1.0
    */
    @FunctionalInterface
    public interface Runnable {
    public abstract void run();
    }

    用着 “三无产品” 总是有一些弊端,其中没办法拿到返回值是最让人不能忍的,于是 Callable 就诞生了
    Callable
    又是 Doug Lea 大师,又是 Java 1.5 这个神奇的版本

    /**
    * @see Executor
    * @since 1.5
    * @author Doug Lea
    * @param <V> the result type of method {@code call}
    */
    @FunctionalInterface
    public interface Callable<V> {
    
    V call() throws Exception;
    }
    Callable 是一个泛型接口,里面只有一个 call() 方法,该方法可以返回泛型值 V ,使用起来就像这样:
    Callable<String> callable = () -> {
    // Perform some computation
    Thread.sleep(2000);
    return "Return some result";
    };

    技术图片
    二者都是函数式接口,里面都仅有一个方法,使用上又是如此相似,除了有无返回值,Runnable 与 Callable 就点差别吗?
    Runnable VS Callable
    两个接口都是用于多线程执行任务的,但他们还是有很明显的差别的
    技术图片
    执行机制
    先从执行机制上来看,Runnable 你太清楚了,它既可以用在 Thread 类中,也可以用在 ExecutorService 类中配合线程池的使用;Bu~~~~t, Callable 只能在 ExecutorService 中使用,你翻遍 Thread 类,也找不到Callable 的身影
    技术图片
    异常处理
    Runnable 接口中的 run 方法签名上没有 throws ,自然也就没办法向上传播受检异常;而 Callable 的 call() 方法签名却有 throws,所以它可以处理受检异常;
    所以归纳起来看主要有这几处不同点:
    技术图片
    整体差别虽然不大,但是这点差别,却具有重大意义
    返回值和处理异常很好理解,另外,在实际工作中,我们通常要使用线程池来管理线程(原因已经在 为什么要使用线程池? 中明确说明),所以我们就来看看 ExecutorService 中是如何使用二者的
    ExecutorService
    先来看一下 ExecutorService 类图
    技术图片
    我将上图标记的方法单独放在此处

    
    void execute(Runnable command);

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

可以看到,使用ExecutorService 的 execute() 方法依旧得不到返回值,而 submit() 方法清一色的返回 Future 类型的返回值
细心的朋友可能已经发现, submit() 方法已经在 CountDownLatch 和 CyclicBarrier 傻傻的分不清楚? 文章中多次使用了,只不过我们没有获取其返回值罢了,那么
Future 到底是什么呢?
怎么通过它获取返回值呢?
我们带着这些疑问一点点来看
Future
Future 又是一个接口,里面只有五个方法:
![](https://s4.51cto.com/images/blog/202007/30/538443537c63e5a8ceb882118f5bcf0a.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
从方法名称上相信你已经能看出这些方法的作用

// 取消任务
boolean cancel(boolean mayInterruptIfRunning);

// 获取任务执行结果
V get() throws InterruptedException, ExecutionException;

// 获取任务执行结果,带有超时时间限制
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;

// 判断任务是否已经取消
boolean isCancelled();

// 判断任务是否已经结束
boolean isDone();

铺垫了这么多,看到这你也许有些乱了,咱们赶紧看一个例子,演示一下几个方法的作用br/>@Slf4j
public class FutureAndCallableExample {

public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newSingleThreadExecutor();

  // 使用 Callable ,可以获取返回值
  Callable<String> callable = () -> {
     log.info("进入 Callable 的 call 方法");
     // 模拟子线程任务,在此睡眠 2s,
     // 小细节:由于 call 方法会抛出 Exception,这里不用像使用 Runnable 的run 方法那样 try/catch 了
     Thread.sleep(5000);
     return "Hello from Callable";
  };

  log.info("提交 Callable 到线程池");
  Future<String> future = executorService.submit(callable);

  log.info("主线程继续执行");

  log.info("主线程等待获取 Future 结果");
  // Future.get() blocks until the result is available
  String result = future.get();
  log.info("主线程获取到 Future 结果: {}", result);

  executorService.shutdown();

}
}

程序运行结果如下:
![](https://s4.51cto.com/images/blog/202007/30/25f9e23ed87f461d8055c670d16905ee.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
如果你运行上述示例代码,主线程调用 future.get() 方法会阻塞自己,直到子任务完成。我们也可以使用 Future 方法提供的 isDone 方法,它可以用来检查 task 是否已经完成了,我们将上面程序做点小修改:

// 如果子线程没有结束,则睡眠 1s 重新检查
while(!future.isDone()) {
System.out.println("Task is still not done...");
Thread.sleep(1000);
}

来看运行结果:
![](https://s4.51cto.com/images/blog/202007/30/0d14300f1925ca702234754c95c1f4db.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
如果子程序运行时间过长,或者其他原因,我们想 cancel 子程序的运行,则我们可以使用 Future 提供的 cancel 方法,继续对程序做一些修改

while(!future.isDone()) {
System.out.println("子线程任务还没有结束...");
Thread.sleep(1000);

double elapsedTimeInSec = (System.nanoTime() - startTime)/1000000000.0;

// 如果程序运行时间大于 1s,则取消子线程的运行
if(elapsedTimeInSec > 1) {
future.cancel(true);
}
}

来看运行结果:
![](https://s4.51cto.com/images/blog/202007/30/55c66d7c69c079ff145623a22dd5f26d.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
为什么调用 cancel 方法程序会出现 CancellationException 呢?是因为调用 get() 方法时,明确说明了:
调用 get() 方法时,如果计算结果被取消了,则抛出 CancellationException (具体原因,你会在下面的源码分析中看到)
![](https://s4.51cto.com/images/blog/202007/30/10d9cc05ef9067a41f2831b9db335116.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
有异常不处理是非常不专业的,所以我们需要进一步修改程序,以更友好的方式处理异常

// 通过 isCancelled 方法判断程序是否被取消,如果被取消,则打印日志,如果没被取消,则正常调用 get() 方法
if (!future.isCancelled()){
log.info("子线程任务已完成");
String result = future.get();
log.info("主线程获取到 Future 结果: {}", result);
}else {
log.warn("子线程任务被取消");
}

查看程序运行结果:
![](https://s4.51cto.com/images/blog/202007/30/16ec2b4497a9fe9b832806eef6a8b0e3.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
相信到这里你已经对 Future 的几个方法有了基本的使用印象,但 Future 是接口,其实使用 ExecutorService.submit() 方法返回的一直都是 Future 的实现类 FutureTask
![](https://s4.51cto.com/images/blog/202007/30/3917f2b964e8c39f370ed852d0fc3730.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
接下来我们就进入这个核心实现类一探究竟
FutureTask
同样先来看类结构
![](https://s4.51cto.com/images/blog/202007/30/52ce5636dfcc8b1e2baf34bf86c07cbe.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}

很神奇的一个接口,FutureTask 实现了 RunnableFuture 接口,而  RunnableFuture  接口又分别实现了 Runnable 和 Future 接口,所以可以推断出 FutureTask 具有这两种接口的特性:
有 Runnable 特性,所以可以用在 ExecutorService 中配合线程池使用
有 Future 特性,所以可以从中获取到执行结果
FutureTask源码分析
如果你完整的看过 AQS 相关分析的文章,你也许会发现,阅读 Java 并发工具类源码,我们无非就是要关注以下这三点:
- 状态 (代码逻辑的主要控制)
- 队列 (等待排队队列)
- CAS (安全的set 值)
脑海中牢记这三点,咱们开始看 FutureTask 源码,看一下它是如何围绕这三点实现相应的逻辑的
文章开头已经提到,实现 Runnable 接口形式创建的线程并不能获取到返回值,而实现 Callable 的才可以,所以 FutureTask 想要获取返回值,必定是和 Callable 有联系的,这个推断一点都没错,从构造方法中就可以看出来:

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

即便在 FutureTask 构造方法中传入的是 Runnable 形式的线程,该构造方法也会通过 Executors.callable 工厂方法将其转换为 Callable 类型:

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

![](https://s4.51cto.com/images/blog/202007/30/277a47394095452f4b0963db41394d21.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
但是 FutureTask 实现的是 Runnable 接口,也就是只能重写 run() 方法,run() 方法又没有返回值,那问题来了:
* FutureTask 是怎样在 run() 方法中获取返回值的?
* 它将返回值放到哪里了?
* get() 方法又是怎样拿到这个返回值的呢?
我们来看一下 run() 方法(关键代码都已标记注释)

public void run() {
// 如果状态不是 NEW,说明任务已经执行过或者已经被取消,直接返回
// 如果状态是 NEW,则尝试把执行线程保存在 runnerOffset(runner字段),如果赋值失败,则直接返回
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
// 获取构造函数传入的 Callable 值
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
// 正常调用 Callable 的 call 方法就可以获取到返回值
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
// 保存 call 方法抛出的异常
setException(ex);
}
if (ran)
// 保存 call 方法的执行结果
set(result);
}
} finally {
runner = null;
int s = state;
// 如果任务被中断,则执行中断处理
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}

run() 方法没有返回值,至于 run() 方法是如何将 call() 方法的返回结果和异常都保存起来的呢?其实非常简单, 就是通过 set(result) 保存正常程序运行结果,或通过 setException(ex) 保存程序异常信息

/* The result to return or exception to throw from get() /
private Object outcome; // non-volatile, protected by state reads/writes

// 保存异常结果
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}

// 保存正常结果
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}

setException 和 set 方法非常相似,都是将异常或者结果保存在 Object 类型的 outcome 变量中,outcome 是成员变量,就要考虑线程安全,所以他们要通过 CAS方式设置 outcome 变量的值,既然是在 CAS 成功后 更改 outcome 的值,这也就是 outcome 没有被 volatile 修饰的原因所在。
![](https://s4.51cto.com/images/blog/202007/30/66259dd51a903e5f75d360c574965dbd.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
保存正常结果值(set方法)与保存异常结果值(setException方法)两个方法代码逻辑,唯一的不同就是 CAS 传入的 state 不同。我们上面提到,state 多数用于控制代码逻辑,FutureTask 也是这样,所以要搞清代码逻辑,我们需要先对 state 的状态变化有所了解

/

IDEA 不为人知的 5 个骚技巧!真香!

2020-07-07

手摸手教你美化微软的新终端Windows Terminal

2020-07-04

CountDownLatch和CyclicBarrier 傻傻的分不清?超长精美图文又来了

2020-06-29

技术图片

不会用Java Future,我怀疑你泡茶没我快, 又是超长图文!!

标签:volatil   method   process   sub   工厂   相关   简单   rip   lambda   

原文地址:https://blog.51cto.com/14888355/2515088

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!