前言

  在前面的两篇博文中,已经介绍利用FutureTask任务的执行流程,以及利用其实现的cancel方法取消任务的情况。本篇就来介绍下,线程任务的结果获取。

系列目录

利用get方法获取程序运行结果

  我们知道利用Future接口的最重要的操作就是要获取任务的结果,而此操作对应的方法就是get。但是问题来了,如果我调用get方法的时候,任务还没有完成呢?答案就是,等它完成,当前线程将被阻塞,直到任务完成(注意,这里说的完成,指的是任务结束,因为异常而结束也算),get方法返回。主线程(不是执行任务的线程)才被唤醒,然后继续运行。

灵活的get方法(带超时时间的get)

  有人可能会问,如果我调用get方法的时候,任务离完成还需要很长时间,那么我主线程不是会浪费一些时间?是的,如果主线程比较忙的话,这样确实主线程的效率。不过还有一个有参的get方法,此方法以等待时长为参数,如果时长结束,任务还没完成,主线程将继续执行,然后会在之后的某个时间再来获取任务结果。(当然如果主线程依赖这个任务结果才能继续执行,那么只能老老实实地等了

FutureTask的阻塞模型

  要想了解get方法的具体实现,必须先弄清楚,它是如何阻塞的。前篇博文已经提到,FutureTask有类型为WaitNode字段waiters,实际上这个waiters引用的是一个以WaitNode为节点的单向链表的头节点。如图所示:

waitNode类代码如下:

  1. static final class WaitNode {
  2. volatile Thread thread; //线程
  3. volatile WaitNode next; //下一个节点
  4. //构造函数获取当前执行线程的引用
  5. WaitNode() { thread = Thread.currentThread(); }
  6. }

WaitNode保留线程引用的作用是什么?

答案是用于任务完成后唤醒等待线程。当FutureTask执行完callable的run方法后,将执行finishCompletion方法通知所有等待线程

  1. private void finishCompletion() {
  2. //遍历等待节点
  3. for (WaitNode q; (q = waiters) != null;) {
  4. //将FutureTask的waiters引用置null
  5. if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
  6. //唤醒所有等待线程
  7. for (;;) {
  8. //取出节点对应的线程
  9. Thread t = q.thread;
  10. if (t != null) {
  11. q.thread = null;
  12. LockSupport.unpark(t); //唤醒对应线程
  13. }
  14. //获取下一个节点
  15. WaitNode next = q.next;
  16. if (next == null)
  17. break;
  18. q.next = null; // unlink to help gc
  19. q = next;
  20. }
  21. break;
  22. }
  23. }
  24.  
  25. //调用钩子函数done,此为空方法,子类可根据需求进行实现
  26. done();
  27.  
  28. callable = null;
  29. }

线程的阻塞方式——park和unPark

  park/unPark也是用于控制线程等待状态的。我们熟悉的,用于控制线程等待状态的还有wait/notify。wait/notify是某个对象的条件队列,要阻塞线程,或者说要加入等待队列,必须先获取对象的锁。

与wait()/notify不同的是,park和unpark直接操作线程,无需获取对象的锁,个人认为这是这里使用park/unPark,而不是wait/notifyAll的原因,因为获取锁需要额外的开销。

get方法的具体实现

以下是FutureTask中get方法的实现

  1. public V get() throws InterruptedException, ExecutionException {
  2. //获取当前任务状态
  3. int s = state;
  4. //如果是NEW或者COMPLETING,也就是还没有结束,就调用awaitDone进行阻塞
  5. if (s <= COMPLETING)
  6. s = awaitDone(false, 0L); //注意,这里的参数,表示非超时等待,如果任务未结束,程序将一直卡在这里
  7. //如果awaitDone返回,也就是任务已经结束,根据任务状态,返回结果
  8. return report(s);
  9. }

以下是get方法中调用到的awaitDone的实现

  1. private int awaitDone(boolean timed, long nanos) throws InterruptedException {
  2. //根据超时时间,计算结束时间点
  3. final long deadline = timed ? System.nanoTime() + nanos : 0L;
  4. //等待节点
  5. WaitNode q = null;
  6. //是否加入等待队列
  7. boolean queued = false;
  8. //这里并不是通过自旋,使方法无法返回。而是利用自旋CAS, 改变状态。如果成功,一次就够了
  9. for (;;) {
  10. //如果此线程被中断,把从节点从等待队列中移除
  11. if (Thread.interrupted()) {
  12. removeWaiter(q);
  13. throw new InterruptedException();
  14. }
  15.  
  16. int s = state;
  17. //如果状态大于COMPLETING,也就是任务已结束,返回任务状态
  18. if (s > COMPLETING) {
  19. if (q != null)
  20. q.thread = null;
  21. return s;
  22. }
  23. else if (s == COMPLETING) // cannot time out yet
  24. Thread.yield();
  25. //第一次循环,q是null,创建节点
  26. else if (q == null)
  27. q = new WaitNode();
  28. //如果还未加入等待队列,就加入。加入等待队列的目的是,当任务完成的时候能够通过句柄及时唤醒正在等待的线程。注意:加入队列的时候,还没有挂起。
  29. else if (!queued)
  30. //q.next = waiters 表达式的返回值 是左侧的值,也就是waiters
  31. //意思是,如果当前对象的waiters的值是waiters, 就将他赋值为q
  32. queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
  33. q.next = waiters, q);
  34. //如果是超时等待,则调用parkNanos, 线程将在指定时间后被唤醒。目的是先挂起线程,时间到了再唤醒出来,此时还在for循环中,将再次执行符合条件的if块
  35. else if (timed) {
  36. nanos = deadline - System.nanoTime();
    //程序被唤醒后,通过这里跳出循环
  37. if (nanos <= 0L) {
  38. removeWaiter(q);
  39. return state;
  40. }
  41. LockSupport.parkNanos(this, nanos);
  42. }
  43. //如果不是超时等待,且已经加入等待队列,这时候利用park将当前线程挂起
  44. else
  45. LockSupport.park(this);
  46. }
  47. }

很多人可能会觉得这个循环体,看着有点迷糊,我刚开始也看得头大。但是我们可以根据几种情境,来查看这几种情境下代码的执行情况。

注:第二个for循环内,第二个if-else块是一个大块,每次只执行一个。

几种执行情境

一、当前线程成功加入等待队列,且被阻塞,一段时间后任务完成,线程被唤醒

二、当前线程加入队列后,还没被阻塞,任务就已经完成了

三、因为其他线程加入等待队列的影响,当前线程未能加入等待队列

这里说明一下,如果其他线程在此线程之前,比较接近的时间,加入了等待队列,由于内存可见性的原因,当前线程看到的waiters值没有及时改变,故与其实际值不同,CAS操作就将失败。

为什么一定要CAS成功?答案是,如果不成功,出现线程安全问题,链表的结构就会一塌糊涂。这里不细谈。

根据任务状态获取结果

  我们已经知道,FutureTask有一个Object字段的outcome,也就是任务执行的结果。当任务完成后,会将结果赋值给它。以下是FutureTask的run方法:

  1. public void run() {
  2. //任务开始执行后,设置FutureTask的runner字段,指明执行它的线程
  3. if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))
  4. return;
  5. try {
  6. //获取具体任务
  7. Callable<V> c = callable;
  8. if (c != null && state == NEW) {
  9. V result;
  10. boolean ran; //任务是否已被运行完
  11. try {
  12. //运行任务
  13. result = c.call();
  14. ran = true;
  15. } catch (Throwable ex) {
  16. result = null;
  17. //如果运行任务过程中出现异常,则ran=false 表示没有运行完成
  18. ran = false;
  19. //设置异常 => 将任务状态设置为异常,并将异常信息赋值给outcome, 也就是任务结果
  20. //这个方法会调用finishCompletion
  21. setException(ex);
  22. }
  23. //如果运行完成,把结果赋值给outcome
  24. if (ran)
  25. set(result); //这个方法会调用finishCompletion
  26. }
  27. } finally {
  28. //既然线程已经"完成"当前任务,就放弃引用,防止影响它执行其他任务
  29. runner = null;
  30. //重新获取任务状态
  31. int s = state;
  32. if (s >= INTERRUPTING)
  33. handlePossibleCancellationInterrupt(s);
  34. }
  35. }

由前文可知,当任务"完成"的时候,获取结果的线程将被唤醒。回到get方法,它将获取到任务的状态,并根据任务状态获取结果。也就是report方法:

  1. private V report(int s) throws ExecutionException {
  2. //获取结果
  3. Object x = outcome;
  4. //如果任务正常完成
  5. if (s == NORMAL)
  6. //强制转换为对应类型并返回
  7. return (V)x;
  8. //如果任务状态为CANCELLED、INTERRUPTING、INTERRUPTED表明是通过cacel方法取消了
  9. //返回已取消异常
  10. if (s >= CANCELLED)
  11. throw new CancellationException();
  12. //如果是因为异常中断的话,抛出具体异常信息
  13. throw new ExecutionException((Throwable)x);
  14. }

揭开Future的神秘面纱——结果获取的更多相关文章

  1. 揭开Future的神秘面纱——任务执行

    前言 此文承接之前的博文 解开Future的神秘面纱之取消任务 补充一些任务执行的一些细节,并从全局介绍程序的运行情况. 系列目录 揭开Future的神秘面纱——任务取消 揭开Future的神秘面纱— ...

  2. 揭开Future的神秘面纱——任务取消

    系列目录: 揭开Future的神秘面纱——任务取消 揭开Future的神秘面纱——任务执行 揭开Future的神秘面纱——结果获取 使用案例 在之前写过的一篇随笔中已经提到了Future的应用场景和特 ...

  3. ASP.NET 运行时详解 揭开请求过程神秘面纱

    对于ASP.NET开发,排在前五的话题离不开请求生命周期.像什么Cache.身份认证.Role管理.Routing映射,微软到底在请求过程中干了哪些隐秘的事,现在是时候揭晓了.抛开乌云见晴天,接下来就 ...

  4. 带你揭开ATM的神秘面纱

    相信大家都用过ATM取过money吧,但是有多少人真正是了解ATM的呢?相信除了ATM从业者外了解的人寥寥无几吧,鄙人作为一个从事ATM软件开发的伪专业人士就站在我的角度为大家揭开ATM的神秘面纱吧. ...

  5. 解开Future的神秘面纱之任务执行

    此文承接之前的博文 解开Future的神秘面纱之取消任务 补充一些任务执行的一些细节,并从全局介绍程序的运行情况. 任务提交到执行的流程 前文我们已经了解到一些Future的实现细节,这里我们来梳理一 ...

  6. 揭开HTTPS的神秘面纱

    摘自:https://www.cnblogs.com/hujingnb/p/11789728.html 揭开HTTPS的神秘面纱   在说HTTP前,一定要先介绍一下HTTP,这家伙应该不用过多说明了 ...

  7. 从一个Demo开始,揭开Netty的神秘面纱

    本文是Netty系列第5篇 上一篇文章我们对于I/O多路复用.Java NIO包 和 Netty 的关系有了全面的认识. 到目前为止,我们已经从I/O模型出发,逐步接触到了Netty框架.这个过程中, ...

  8. SparkSQL大数据实战:揭开Join的神秘面纱

    本文来自 网易云社区 . Join操作是数据库和大数据计算中的高级特性,大多数场景都需要进行复杂的Join操作,本文从原理层面介绍了SparkSQL支持的常见Join算法及其适用场景. Join背景介 ...

  9. 揭开Docker的神秘面纱

    Docker 相信在飞速发展的今天已经越来越火,它已成为如今各大企业都争相使用的技术.那么Docker 是什么呢?为什么这么多人开始使用Docker? 本节课我们将一起解开Docker的神秘面纱. 本 ...

随机推荐

  1. 设置customer_id

    update t_user_identification u set u.customer_id = (select c.customer_id from t_customer c from t_us ...

  2. eclipse选中某个字段没法高亮其他相同字段

    eclipse选中某个字段无法高亮其他相同字段解决办法: window >> preference >> java >> Editor >> Mark ...

  3. 20155326 2016-2017-2《Java程序设计》课程总结

    20155326 2016-2017-2<Java程序设计>课程总结 (按顺序)每周作业链接汇总 20155326刘美岑的第一次作业:第一次写博客,写下了对java的期待 20155326 ...

  4. 【python3+request】python3+requests接口自动化测试框架实例详解教程

    转自:https://my.oschina.net/u/3041656/blog/820023 [python3+request]python3+requests接口自动化测试框架实例详解教程 前段时 ...

  5. spark checkpoint详解

    checkpoint在spark中主要有两块应用:一块是在spark core中对RDD做checkpoint,可以切断做checkpoint RDD的依赖关系,将RDD数据保存到可靠存储(如HDFS ...

  6. ORACLE ERP consolidation流程(一)

    原文地址:ORACLE ERP consolidation流程(一) 作者:wolfyuan ORACLE EBS by transaction consolidation的详细流程(一)[@more ...

  7. 应该知道的Linux技巧【转】

    这篇文章来源于Quroa的一个问答<What are some time-saving tips that every Linux user should know?>—— Linux用户 ...

  8. EntityFramework Core 学习扫盲

    0. 写在前面 1. 建立运行环境 2. 添加实体和映射数据库 1. 准备工作 2. Data Annotations 3. Fluent Api 3. 包含和排除实体类型 1. Data Annot ...

  9. Kali Linux渗透测试实战 1.3 渗透测试的一般化流程

    1.3 渗透测试的一般化流程 凡事预则立,不预则废,做任何事情都要有一个预先的计划.渗透测试作为测试学科的一个分支,早已形成了完整的方法论.在正式开始本书的实践教学章节之前,我也想谈一谈使用Kali ...

  10. dialog里屏蔽ESC和回车

    重载PreTranslateMessage,在return之前加一句判断,只要是按下ESC和回车的消息,就直接置之不理即可,代码如下: if( pMsg->message == WM_KEYDO ...