(手机横屏看源码更方便)

注:java源码分析部分如无特殊说明均基于 java8 版本。

注:线程池源码部分如无特殊说明均指ThreadPoolExecutor类。

简介

前面我们一起学习了线程池中普通任务的执行流程,但其实线程池中还有一种任务,叫作未来任务(future task),使用它您可以获取任务执行的结果,它是怎么实现的呢?

建议学习本章前先去看看彤哥之前写的《死磕 java线程系列之自己动手写一个线程池(续)》,有助于理解本章的内容,且那边的代码比较短小,学起来相对容易一些。

问题

(1)线程池中的未来任务是怎么执行的?

(2)我们能学到哪些比较好的设计模式?

(3)对我们未来学习别的框架有什么帮助?

来个栗子

我们还是从一个例子入手,来讲解来章的内容。

我们定义一个线程池,并使用它提交5个任务,这5个任务分别返回0、1、2、3、4,在未来的某一时刻,我们再取用它们的返回值,做一个累加操作。

public class ThreadPoolTest02 {public static void main(String[] args) throws ExecutionException, InterruptedException {// 新建一个固定5个线程的线程池ExecutorService threadPool = Executors.newFixedThreadPool(5);List<Future<Integer>> futureList = new ArrayList<>();// 提交5个任务,分别返回0、1、2、3、4for (int i = 0; i < 5; i  ) {int num = i;// 任务执行的结果用Future包装Future<Integer> future = threadPool.submit(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("return: "   num);// 返回值return num;});// 把future添加到list中futureList.add(future);}// 任务全部提交完再从future中get返回值,并做累加int sum = 0;for (Future<Integer> future : futureList) {sum  = future.get();}System.out.println("sum="   sum);}
}

这里我们思考两个问题:

(1)如果这里使用普通任务,要怎么写,时间大概是多少?

如果使用普通任务,那么就要把累加操作放到任务里面,而且并不是那么好写(final的问题),总时间大概是1秒多一点。但是,这样有一个缺点,就是累加操作跟任务本身的内容耦合到一起了,后面如果改成累乘,还要修改任务的内容。

(2)如果这里把future.get()放到for循环里面,时间大概是多少?

这个问题我们先不回答,先来看源码分析。

submit()方法

submit方法,它是提交有返回值任务的一种方式,内部使用未来任务(FutureTask)包装,再交给execute()去执行,最后返回未来任务本身。

public <T> Future<T> submit(Callable<T> task) {// 非空检测if (task == null) throw new NullPointerException();// 包装成FutureTaskRunnableFuture<T> ftask = newTaskFor(task);// 交给execute()方法去执行execute(ftask);// 返回futureTaskreturn ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {// 将普通任务包装成FutureTaskreturn new FutureTask<T>(callable);
}

这里的设计很巧妙,实际上这两个方法都是在AbstractExecutorService这个抽象类中完成的,这是模板方法的一种运用。

我们来看看FutureTask的继承体系:

FutureTask实现了RunnableFuture接口,而RunnableFuture接口组合了Runnable接口和Future接口的能力,而Future接口提供了get任务返回值的能力。

问题:submit()方法返回的为什么是Future接口而不是RunnableFuture接口或者FutureTask类呢?

答:这是因为submit()返回的结果,对外部调用者只想暴露其get()的能力(Future接口),而不想暴露其run()的能力(Runaable接口)。

FutureTask类的run()方法

经过上一章的学习,我们知道execute()方法最后调用的是task的run()方法,上面我们传进去的任务,最后被包装成了FutureTask,也就是说execute()方法最后会调用到FutureTask的run()方法,所以我们直接看这个方法就可以了。

public void run() {// 状态不为NEW,或者修改为当前线程来运行这个任务失败,则直接返回if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))return;try {// 真正的任务Callable<V> c = callable;// state必须为NEW时才运行if (c != null && state == NEW) {// 运行的结果V result;boolean ran;try {// 任务执行的地方【本文由公从号“彤哥读源码”原创】result = c.call();// 已执行完毕ran = true;} catch (Throwable ex) {result = null;ran = false;// 处理异常setException(ex);}if (ran)// 处理结果set(result);}} finally {// 置空runnerrunner = null;// 处理中断int s = state;if (s >= INTERRUPTING)handlePossibleCancellationInterrupt(s);}
}

可以看到代码也比较简单,先做状态的检测,再执行任务,最后处理结果或异常。

执行任务这里没啥问题,让我们看看处理结果或异常的代码。

protected void setException(Throwable t) {// 将状态从NEW置为COMPLETINGif (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {// 返回值置为传进来的异常(outcome为调用get()方法时返回的)outcome = t;// 最终的状态设置为EXCEPTIONALUNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state// 调用完成方法finishCompletion();}
}
protected void set(V v) {// 将状态从NEW置为COMPLETINGif (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {// 返回值置为传进来的结果(outcome为调用get()方法时返回的)outcome = v;// 最终的状态设置为NORMALUNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state// 调用完成方法finishCompletion();}
}

咋一看,这两个方法似乎差不多,不同的是出去的结果不一样且状态不一样,最后都调用了finishCompletion()方法。

private void finishCompletion() {// 如果队列不为空(这个队列实际上为调用者线程)for (WaitNode q; (q = waiters) != null;) {// 置空队列if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {for (;;) {// 调用者线程Thread t = q.thread;if (t != null) {q.thread = null;// 如果调用者线程不为空,则唤醒它// 【本文由公从号“彤哥读源码”原创】LockSupport.unpark(t);}WaitNode next = q.next;if (next == null)break;q.next = null; // unlink to help gcq = next;}break;}}// 钩子方法,子类重写done();// 置空任务callable = null;        // to reduce footprint
}

整个run()方法总结下来:

(1)FutureTask有一个状态state控制任务的运行过程,正常运行结束state从NEW->COMPLETING->NORMAL,异常运行结束state从NEW->COMPLETING->EXCEPTIONAL;

(2)FutureTask保存了运行任务的线程runner,它是线程池中的某个线程;

(3)调用者线程是保存在waiters队列中的,它是什么时候设置进去的呢?

(4)任务执行完毕,除了设置状态state变化之外,还要唤醒调用者线程。

调用者线程是什么时候保存在FutureTask中(waiters)的呢?查看构造方法:

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

发现并没有相关信息,我们再试想一下,如果调用者不调用get()方法,那么这种未来任务是不是跟普通任务没有什么区别?确实是的哈,所以只有调用get()方法了才有必要保存调用者线程到FutureTask中。

所以,我们来看看get()方法中是什么鬼。

FutureTask类的get()方法

get()方法调用时如果任务未执行完毕,会阻塞直到任务结束。

public V get() throws InterruptedException, ExecutionException {int s = state;// 如果状态小于等于COMPLETING,则进入队列等待if (s <= COMPLETING)s = awaitDone(false, 0L);// 返回结果(异常)return report(s);
}

是不是很清楚,如果任务状态小于等于COMPLETING,则进入队列等待。

private int awaitDone(boolean timed, long nanos)throws InterruptedException {// 我们这里假设不带超时final long deadline = timed ? System.nanoTime()   nanos : 0L;WaitNode q = null;boolean queued = false;for (;;) {// 处理中断if (Thread.interrupted()) {removeWaiter(q);throw new InterruptedException();}// 4. 如果状态大于COMPLETING了,则跳出循环并返回// 这是自旋的出口int s = state;if (s > COMPLETING) {if (q != null)q.thread = null;return s;}// 如果状态等于COMPLETING,说明任务快完成了,就差设置状态到NORMAL或EXCEPTIONAL和设置结果了// 这时候就让出CPU,优先完成任务else if (s == COMPLETING) // cannot time out yetThread.yield();// 1. 如果队列为空else if (q == null)// 初始化队列(WaitNode中记录了调用者线程)q = new WaitNode();// 2. 未进入队列else if (!queued)// 尝试入队queued = UNSAFE.compareAndSwapObject(this, waitersOffset,q.next = waiters, q);// 超时处理else if (timed) {nanos = deadline - System.nanoTime();if (nanos <= 0L) {removeWaiter(q);return state;}LockSupport.parkNanos(this, nanos);}// 3. 阻塞当前线程(调用者线程)else// 【本文由公从号“彤哥读源码”原创】LockSupport.park(this);}
}

这里我们假设调用get()时任务还未执行,也就是其状态为NEW,我们试着按上面标示的1、2、3、4走一遍逻辑:

(1)第一次循环,状态为NEW,直接到1处,初始化队列并把调用者线程封装在WaitNode中;

(2)第二次循环,状态为NEW,队列不为空,到2处,让包含调用者线程的WaitNode入队;

(3)第三次循环,状态为NEW,队列不为空,且已入队,到3处,阻塞调用者线程;

(4)假设过了一会任务执行完毕了,根据run()方法的分析最后会unpark调用者线程,也就是3处会被唤醒;

(5)第四次循环,状态肯定大于COMPLETING了,退出循环并返回;

问题:为什么要在for循环中控制整个流程呢,把这里的每一步单独拿出来写行不行?

答:因为每一次动作都需要重新检查状态state有没有变化,如果拿出去写也是可以的,只是代码会非常冗长。这里只分析了get()时状态为NEW,其它的状态也可以自行验证,都是可以保证正确的,甚至两个线程交叉运行(断点的技巧)。

OK,这里返回之后,再看看是怎么处理最终的结果的。

private V report(int s) throws ExecutionException {Object x = outcome;// 任务正常结束if (s == NORMAL)return (V)x;// 被取消了if (s >= CANCELLED)throw new CancellationException();// 执行异常throw new ExecutionException((Throwable)x);
}

还记得前面分析run的时候吗,任务执行异常时是把异常放在outcome里面的,这里就用到了。

(1)如果正常执行结束,则返回任务的返回值;

(2)如果异常结束,则包装成ExecutionException异常抛出;

通过这种方式,线程中出现的异常也可以返回给调用者线程了,不会像执行普通任务那样调用者是不知道任务执行到底有没有成功的。

其它

FutureTask除了可以获取任务的返回值以外,还能够取消任务的执行。

public boolean cancel(boolean mayInterruptIfRunning) {if (!(state == NEW &&UNSAFE.compareAndSwapInt(this, stateOffset, NEW,mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))return false;try {    // in case call to interrupt throws exceptionif (mayInterruptIfRunning) {try {Thread t = runner;if (t != null)t.interrupt();} finally { // final stateUNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);}}} finally {finishCompletion();}return true;
}

这里取消任务是通过中断执行线程来处理的,有兴趣的同学可以自己分析一下。

回答开篇

如果这里把future.get()放到for循环里面,时间大概是多少?

答:大概会是5秒多一点,因为每提交一个任务,都要阻塞调用者线程直到任务执行完毕,每个任务执行都是1秒多,所以总时间就是5秒多点。

总结

(1)未来任务是通过把普通任务包装成FutureTask来实现的。

(2)通过FutureTask不仅能够获取任务执行的结果,还有感知到任务执行的异常,甚至还可以取消任务;

(3)AbstractExecutorService中定义了很多模板方法,这是一种很重要的设计模式;

(4)FutureTask其实就是典型的异常调用的实现方式,后面我们学习到Netty、Dubbo的时候还会见到这种设计思想的。

彩蛋

RPC框架中异步调用是怎么实现的?

答:RPC框架常用的调用方式有同步调用、异步调用,其实它们本质上都是异步调用,它们就是用FutureTask的方式来实现的。

一般地,通过一个线程(我们叫作远程线程)去调用远程接口,如果是同步调用,则直接让调用者线程阻塞着等待远程线程调用的结果,待结果返回了再返回;如果是异步调用,则先返回一个未来可以获取到远程结果的东西FutureXxx,当然,如果这个FutureXxx在远程结果返回之前调用了get()方法一样会阻塞着调用者线程。

有兴趣的同学可以先去预习一下dubbo的异步调用(它是把Future扔到RpcContext中的)。

欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java线程系列之线程池深入解析——未来任务执行流程相关推荐

  1. 死磕 java同步系列之ReentrantReadWriteLock源码解析

    问题 (1)读写锁是什么? (2)读写锁具有哪些特性? (3)ReentrantReadWriteLock是怎么实现读写锁的? (4)如何使用ReentrantReadWriteLock实现高效安全的 ...

  2. java condition_死磕 java同步系列之ReentrantLock源码解析(二)

    (手机横屏看源码更方便) 问题 (1)条件锁是什么? (2)条件锁适用于什么场景? (3)条件锁的await()是在其它线程signal()的时候唤醒的吗? 简介 条件锁,是指在获取锁之后发现当前业务 ...

  3. phaser java_死磕 java同步系列之Phaser源码解析

    问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这 ...

  4. 死磕java_死磕 java同步系列之AQS终篇(面试)

    问题 (1)AQS的定位? (2)AQS的重要组成部分? (3)AQS运用的设计模式? (4)AQS的总体流程? 简介 AQS的全称是AbstractQueuedSynchronizer,它的定位是为 ...

  5. 死磕 java同步系列之redis分布式锁进化史

    问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...

  6. 死磕 java同步系列之终结篇

    简介 同步系列到此就结束了,本篇文章对同步系列做一个总结. 脑图 下面是关于同步系列的一份脑图,列举了主要的知识点和问题点,看过本系列文章的同学可以根据脑图自行回顾所学的内容,也可以作为面试前的准备. ...

  7. java 同步锁_死磕 java同步系列之自己动手写一个锁Lock

    问题 (1)自己动手写一个锁需要哪些知识? (2)自己动手写一个锁到底有多简单? (3)自己能不能写出来一个完美的锁? 简介 本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁. ...

  8. 死磕 java同步系列之AQS起篇

    问题 (1)AQS是什么? (2)AQS的定位? (3)AQS的实现原理? (4)基于AQS实现自己的锁? 简介 AQS的全称是AbstractQueuedSynchronizer,它的定位是为Jav ...

  9. 死磕java_死磕 java同步系列之AQS起篇

    问题 (1)AQS是什么? (2)AQS的定位? (3)AQS的实现原理? (4)基于AQS实现自己的锁? 简介 AQS的全称是AbstractQueuedSynchronizer,它的定位是为Jav ...

最新文章

  1. 一个比特币要挖多久?
  2. java android 数组_android中数组的定义与使用
  3. 软件测试 图覆盖,软件测试(四)——图覆盖
  4. 微服务架构,如何做分布式,通用缓存机制?
  5. __init__函数
  6. HDU - 1527 取石子游戏(威佐夫博弈)
  7. 工具栏自定义_EXCEL LESSON12 自定义功能区菜单及工具栏(1/3)
  8. 将数组作为参数,调用该函数时候给的是数组地址还是整个数组
  9. 每周四JEECG社区公开课:微信公众账号运营(jeewx使用)入门讲解
  10. 再见 Docker !5分钟转型 containerd !
  11. JVM调优之:垃圾收集器
  12. logit回归怎么看显著性_spss logistic回归分析结果如何分析
  13. 什么是信息安全管理体系
  14. 极小曲面壳体的静力学分析(ABAQUS)
  15. 随机森林回归简单示例
  16. Linux上 如何查找yum安装包所缺缺少的依赖包及报错处理
  17. 《系统与网络管理实践》(第三版)作者访谈
  18. aquarius Java自定义对象池
  19. 《操作系统》学习笔记|6.6外存空间管理
  20. C语言之strcmp函数和strncmp函数

热门文章

  1. 解决IE浏览器处理返回JSON数据提示下载问题
  2. 深度学习方法在负荷预测中的应用综述(论文阅读)
  3. 仓库摆放示意图_仓库合理堆放标准,货物入库摆放规范
  4. 使用MySQL可视化客户端,例如SQLyog,Navicat等,只编写SQL语句,使用2的N次方原理,快速初始化百万千万条数据
  5. Linux发展史之简要概述
  6. ActionEnglish Notes
  7. 数据库MYSQL及MYSQL ODBC
  8. 关于IOS中uni.downloadFile下载的图片显示不出来的解决方法
  9. 怎样补充nmn,nmn胶囊正确服用方法,现在知道还不晚
  10. Vue css样式穿透和权重