将一个任务的状态设置成终止态只有三种方法:
- set
- setException
- cancel
前两种方法已经分析完,接下来我们就看一下 cancel
方法
查看 Future cancel(),该方法注释上明确说明三种 cancel 操作一定失败的情形
- 任务已经执行完成了
- 任务已经被取消过了
- 任务因为某种原因不能被取消
其它情况下,cancel操作将返回true。值得注意的是,cancel操作返回 true 并不代表任务真的就是被取消, 这取决于发动cancel状态时,任务所处的状态
- 如果发起cancel时任务还没有开始运行,则随后任务就不会被执行;
- 如果发起cancel时任务已经在运行了,则这时就需要看
mayInterruptIfRunning
参数了:
- 如果mayInterruptIfRunning 为true, 则当前在执行的任务会被中断
- 如果mayInterruptIfRunning 为false, 则可以允许正在执行的任务继续运行,直到它执行完
有了这些铺垫,看一下 cancel 代码的逻辑就秒懂了
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 exception // 需要中断任务执行线程 if (mayInterruptIfRunning) { try { Thread t = runner; // 中断线程 if (t != null) t.interrupt(); } finally { // final state // 修改为最终状态 INTERRUPTED UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); } } } finally { // 唤醒等待中的线程 finishCompletion(); } return true; }
核心方法终于分析完了,到这咱们喝口茶休息一下吧
我是想说,使用 FutureTask 来演练烧水泡茶经典程序
如上图:
- 洗水壶 1 分钟
- 烧开水 15 分钟
- 洗茶壶 1 分钟
- 洗茶杯 1 分钟
- 拿茶叶 2 分钟
最终泡茶
让我心算一下,如果串行总共需要 20 分钟,但很显然在烧开水期间,我们可以洗茶壶/洗茶杯/拿茶叶
这样总共需要 16 分钟,节约了 4分钟时间,烧水泡茶尚且如此,在现在高并发的时代,4分钟可以做的事太多了,学会使用 Future 优化程序是必然(其实优化程序就是寻找关键路径,关键路径找到了,非关键路径的任务通常就可以和关键路径的内容并行执行了)
@Slf4j public class MakeTeaExample { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(2); // 创建线程1的FutureTask FutureTask<String> ft1 = new FutureTask<String>(new T1Task()); // 创建线程2的FutureTask FutureTask<String> ft2 = new FutureTask<String>(new T2Task()); executorService.submit(ft1); executorService.submit(ft2); log.info(ft1.get() + ft2.get()); log.info("开始泡茶"); executorService.shutdown(); } static class T1Task implements Callable<String> { @Override public String call() throws Exception { log.info("T1:洗水壶..."); TimeUnit.SECONDS.sleep(1); log.info("T1:烧开水..."); TimeUnit.SECONDS.sleep(15); return "T1:开水已备好"; } } static class T2Task implements Callable<String> { @Override public String call() throws Exception { log.info("T2:洗茶壶..."); TimeUnit.SECONDS.sleep(1); log.info("T2:洗茶杯..."); TimeUnit.SECONDS.sleep(2); log.info("T2:拿茶叶..."); TimeUnit.SECONDS.sleep(1); return "T2:福鼎白茶拿到了"; } } }
上面的程序是主线程等待两个 FutureTask 的执行结果,线程1 烧开水时间更长,线程1希望在水烧开的那一刹那就可以拿到茶叶直接泡茶,怎么半呢?
那只需要在线程 1 的FutureTask 中获取 线程 2 FutureTask 的返回结果就可以了,我们稍稍修改一下程序:
@Slf4j public class MakeTeaExample1 { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(2); // 创建线程2的FutureTask FutureTask<String> ft2 = new FutureTask<String>(new T2Task()); // 创建线程1的FutureTask FutureTask<String> ft1 = new FutureTask<String>(new T1Task(ft2)); executorService.submit(ft1); executorService.submit(ft2); executorService.shutdown(); } static class T1Task implements Callable<String> { private FutureTask<String> ft2; public T1Task(FutureTask<String> ft2) { this.ft2 = ft2; } @Override public String call() throws Exception { log.info("T1:洗水壶..."); TimeUnit.SECONDS.sleep(1); log.info("T1:烧开水..."); TimeUnit.SECONDS.sleep(15); String t2Result = ft2.get(); log.info("T1 拿到T2的 {}, 开始泡茶", t2Result); return "T1: 上茶!!!"; } } static class T2Task implements Callable<String> { @Override public String call() throws Exception { log.info("T2:洗茶壶..."); TimeUnit.SECONDS.sleep(1); log.info("T2:洗茶杯..."); TimeUnit.SECONDS.sleep(2); log.info("T2:拿茶叶..."); TimeUnit.SECONDS.sleep(1); return "福鼎白茶"; } } }
来看程序运行结果:
知道这个变化后我们再回头看 ExecutorService 的三个 submit 方法:
<T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task); <T> Future<T> submit(Callable<T> task);
第一种方法,逐层代码查看到这里:
你会发现,和我们改造烧水泡茶的程序思维是相似的,可以传进去一个 result,result 相当于主线程和子线程之间的桥梁,通过它主子线程可以共享数据
第二个方法参数是 Runnable 类型参数,即便调用 get() 方法也是返回 null,所以仅是可以用来断言任务已经结束了,类似 Thread.join()
第三个方法参数是 Callable 类型参数,通过get() 方法可以明确获取 call() 方法的返回值
到这里,关于 Future 的整块讲解就结束了,还是需要简单消化一下的
总结
如果熟悉 Javascript 的朋友,Future 的特性和 Javascript 的 Promise 是类似的,私下开玩笑通常将其比喻成男朋友的承诺
回归到Java,我们从 JDK 的演变历史,谈及 Callable 的诞生,它弥补了 Runnable 没有返回值的空缺,通过简单的 demo 了解 Callable 与 Future 的使用。 FutureTask 又是 Future接口的核心实现类,通过阅读源码了解了整个实现逻辑,最后结合FutureTask 和线程池演示烧水泡茶程序,相信到这里,你已经可以轻松获取线程结果了
烧水泡茶是非常简单的,如果更复杂业务逻辑,以这种方式使用 Future 必定会带来很大的会乱(程序结束没办法主动通知,Future 的链接和整合都需要手动操作)为了解决这个短板,没错,又是那个男人 Doug Lea, CompletableFuture
工具类在 Java1.8 的版本出现了,搭配 Lambda 的使用,让我们编写异步程序也像写串行代码那样简单,纵享丝滑
接下来我们就了解一下 CompletableFuture
的使用
灵魂追问
- 你在日常开发工作中是怎样将整块任务做到分工与协作的呢?有什么基本准则吗?
- 如何批量的执行异步任务呢?