CompletableFuture尽管在2014年的三月随着Java8被提出来,但它现在仍然是一种相对较新潮的概念。但也许这个类不为人所熟知是好事,因为它很容易被滥用,特别是涉及到使用线程和线程池的时候。而这篇文章的目的就是要描述线程是怎样使用CompletableFuture的。

Running tasks

这是API的基础部分,它有一个很实用的supplyAsync()方法,这个方法和ExecutorService.submit()很像,但不同的是返回CompletableFuture:

CompletableFuture.supplyAsync(() -> {
try (InputStream is = new URL("http://www.cnblogs.com").openStream()) {
log.info("Downloading");
return IOUtils.toString(is, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
});

问题是supplyAsync()默认使用 ForkJoinPool.commonPool(),线程池由所有的CompletableFutures分享,所有的并行流和所有的应用都部署在同一个虚拟机上(如果你很不幸的仍在使用有很多人工部署的应用服务器)。这种硬编码的,不可配置的线程池完全超出了我们的控制,很难去监测和度量。因此你应该指定你自己的Executor,就像这里(也可以看看这里几种创造这样Exetutor的方法):

ExecutorService pool = Executors.newFixedThreadPool(10);
final CompletableFuture future =
CompletableFuture.supplyAsync(() -> {
//...
}, pool);

这仅仅是开始…

Callbacks and transformations

假如你想转换给定的CompletableFuture,例如提取String的长度:

CompletableFuture intFuture =
future.thenApply(s -> s.length());

那么是谁调用了s.length()?坦白点,我一点也不在乎。只要涉及到lambda表达式,那么所有的执行者像thenApply这样的就是廉价的,我们并不关心是谁调用了lambda表达式。但如果这样的表达式会占用一点点的CPU来完成阻塞的网络通信那又会如何呢?

首先默认情况下会发生什么?试想一下:我们有一个返回String类型的后台任务,当结果完成时我们想要异步地去执行特定的变换。最容易的实现方法是通过包装一个原始的任务(返回String),任务完成时截获它。当内部的task结束后,回调就开始执行,执行变换和返回改进的值。就像有一个面介于我们的代码和初始的计算结果之间(个人看法:这里指的是下面的future里面包含的task执行完毕返回结果s,然后立马执行callback也就是thenApply里面的lambda表达式,这也就是为什么作者说有一个面位于初始计算结果和回调执行代码之间)。那就是说这应该相当明显了,s.length()的变换会在和执行原始任务相同的线程里完成,哈?并不完全是这样!(这里指的是有时候变换的线程和执行原始任务的线程不是同一个线程,看下面就知道)

CompletableFuture future =
CompletableFuture.supplyAsync(() -> {
sleepSeconds(2);
return "ABC";
}, pool); future.thenApply(s -> {
log.info("First transformation");
return s.length();
}); future.get();
pool.shutdownNow();
pool.awaitTermination(1, TimeUnit.MINUTES); future.thenApply(s -> {
log.info("Second transformation");
return s.length();
});

如果future里面的task还在运行,那么包含first transformation的 thenApply()就会一直处于挂起状态。而这个task完成后thenApply()会立即执行,执行的线程和执行task的线程是同一个。然而在注册第二次变换之前(也就是执行第二个thenApply()),我们将一直等待直到task完成(和第一个变换是一样的,都需要等待)。更坏的情况是,我们完全地关闭了线程池,保证其他的代码将不会执行。那么哪个线程将要执行二次变换呢?我们都知道当注册了callback的future完成时,二次变换必定会立刻执行。这就是说它是使用默认的主线程(来完成callback),上面的代码输出如下:

pool-1-thread-1 | First transformation      main | Second transformation

二次变换在注册的时候就意识到CompletableFuture已经完成了(指的是future里面的task已经返回结果,其实在第一次调用thenApply()之前就已经返回了,所以这一次不用等待task),因此它立刻执行了变换。由于此时已经没有其他的线程,所以thenApply()就只能在当前的main线程环境中被调用。最主要的原因还是因为这种行为机制在实际的变换成本很高时(如很耗时)很容易出错。想象一下thenApply()内部的lambda表达式在进行一些繁重的计算或者阻塞的网络调用,突然我们的异步 CompletableFuture阻塞了调用者线程!

Controlling callback’s thread pool

有两种技术去控制执行回调和变换的线程,需要注意的是这些方法仅仅适用你的变换需要很高成本的时候,其他情况下可以忽略。那么第一个方法可以选择使用操作者的 *Async方法,例如:

future.thenApplyAsync(s -> {
log.info("Second transformation");
return s.length();
});

这一次second transformation被自动地卸载到了我们的老朋友线程ForkJoinPool.commonPool()中去了:

pool-1-thread-1                  | First transformation
ForkJoinPool.commonPool-worker-1 | Second transformation

但我们并不喜欢commonPool,所以我们提供自己的:

future.thenApplyAsync(s -> {
log.info("Second transformation");
return s.length();
}, pool2);

注意到这里使用的是不同的线程池(pool-1 vs. pool-2):

pool-1-thread-1 | First transformation
pool-2-thread-1 | Second transformation

Treating callback like another computation step

我相信如果你在处理一些长时间运行的callbacks和transformations上有些麻烦(记住这篇文章同样也适用于CompletableFuture的其他大部分方法),你应该简单地使用其他表意明确的CompletableFuture,就像这样:

//Imagine this is slow and costly
CompletableFuture<Integer> strLen(String s) {
return CompletableFuture.supplyAsync(
() -> s.length(),
pool2);
} //... CompletableFuture<Integer> intFuture =
future.thenCompose(s -> strLen(s));

这种方法更加明确,知道我们的变换有很大的开销,我们不会将它运行在一些随意的不可控的线程上。取而代之的是我们会将String到CompletableFuture<Integer>的变换封装为一个异步操作。然而,我们必须用thenCompose()取代thenApply(),否则的话我们会得到CompletableFuture<CompletableFuture<Integer>>.

但如果我们的transformation 没有一个能够很好地处理嵌套CompletableFuture的形式怎么办,如applyToEither()会等待第一个Future完成然后执行transformation.

CompletableFuture<CompletableFuture<Integer>> poor =
future1.applyToEither(future2, s -> strLen(s));

这里有个很实用的技巧,用来“展开”这类难以理解的数据结构,这种技巧叫flatten,通过使用flatMap(identity) (or flatMap(x -> x))。在我们的例子中flatMap()就叫做thenCompose:

CompletableFuture<Integer> good =
poor.thenCompose(x -> x);

我把它留给你,去弄懂它是怎样和为什么这样工作的。我想这篇文章已经尽量清楚地阐述了线程是如何参与到CompletableFuture中去的。

 

哪个线程执行 CompletableFuture’s tasks 和 callbacks?的更多相关文章

  1. java并发编程学习:如何等待多个线程执行完成后再继续后续处理(synchronized、join、FutureTask、CyclicBarrier)

    多线程应用中,经常会遇到这种场景:后面的处理,依赖前面的N个线程的处理结果,必须等前面的线程执行完毕后,后面的代码才允许执行. 在我不知道CyclicBarrier之前,最容易想到的就是放置一个公用的 ...

  2. Java自定义线程池-记录每个线程执行耗时

    ThreadPoolExecutor是可扩展的,其提供了几个可在子类化中改写的方法,如下: protected void beforeExecute(Thread t, Runnable r) { } ...

  3. java高并发系列 - 第31天:获取线程执行结果,这6种方法你都知道?

    这是java高并发系列第31篇. 环境:jdk1.8. java高并发系列已经学了不少东西了,本篇文章,我们用前面学的知识来实现一个需求: 在一个线程中需要获取其他线程的执行结果,能想到几种方式?各有 ...

  4. Java多线程--让主线程等待子线程执行完毕

    使用Java多线程编程时经常遇到主线程需要等待子线程执行完成以后才能继续执行,那么接下来介绍一种简单的方式使主线程等待. java.util.concurrent.CountDownLatch 使用c ...

  5. 驱动插ring3线程执行代码

    近日有在写一个小东西 需要在内核态中运行一个WIN32程序 之前提到的插入APC可以满足部分要求 但是一到WIN7 x86平台下就崩溃了WIN7下只能插入第三方的进程 一插入系统进程就崩溃,但是这样满 ...

  6. 卸载AppDomain动态调用DLL异步线程执行失败

    应用场景 动态调用DLL中的类,执行类的方法实现业务插件功能 使用Assembly 来实现 但是会出现逻辑线程数异常的问题 使用AppDomain 实现动态调用,并卸载. 发现问题某个插件中开启异步线 ...

  7. 指定线程执行的顺序---join()

    线程T1,T2,T3分别启动,如何让其执行顺序变为T3>T2>T1: 线程1: package test6; public class Thread1 extends Thread{ pr ...

  8. java并发:获取线程执行结果(Callable、Future、FutureTask)

    初识Callable and Future 在编码时,我们可以通过继承Thread或是实现Runnable接口来创建线程,但是这两种方式都存在一个缺陷:在执行完任务之后无法获取执行结果.如果需要获取执 ...

  9. Java多线程——<三>简单的线程执行:Executor

    一.概述 按照<Java多线程——<一><二>>中所讲,我们要使用线程,目前都是显示的声明Thread,并调用其start()方法.多线程并行,明显我们需要声明多个 ...

随机推荐

  1. 发一个比trace功能更强大debug工具,MonterDebugger

    经常看到兄弟说trace不出东西啊,这样给你调试会带来很多不便:加入说我们需要将运行时的debug信息和之前某个版本的进行比对:又加入说我们需要在运行时通过debug动态调整显示对象的属性:查看当前整 ...

  2. Python结合Shell/Hadoop实现MapReduce

    基本流程为: cat data | map | sort | reduce cat devProbe | ./mapper.py | sort| ./reducer.py echo "foo ...

  3. 使用神经网络识别手写数字Using neural nets to recognize handwritten digits

    The human visual system is one of the wonders of the world. Consider the following sequence of handw ...

  4. Java9 modules (Jigsaw)模块化迁移

    要点 通过模块化的方式开发应用程序,实现更好的设计,如关注点分离和封装性. 通过Java平台模块化系统(JPMS),开发者可以定义他们的应用程序模块,决定其他模块如何调用他们的模块,以及他们的模块如何 ...

  5. 深入理解JS函数节流和去抖动

    一.什么是节流和去抖? 1.节流 节流就是拧紧水龙头让水少流一点,但是不是不让水流了.想象一下在现实生活中有时候我们需要接一桶水,接水的同时不想一直站在那等着,可能要离开一会去干一点别的事请,让水差不 ...

  6. 数据库建模软件ERStudio-表关系建模详解

    ERStudio是优秀的数据库建模软件,它不仅可以建立表.视图等模型,还可以建立多表间各种关系的模型,另外还可以根据模型生成表到数据库,下面具体讲解一下它的表关系建模. 1. 首先讲一下怎么建立表关系 ...

  7. block的知识点

    // //  main.m //  1211块练习 // //  Created by jerehedu on 14/12/11. //  Copyright (c) 2014年 jereh. All ...

  8. AutoCAD .NET二次开发(二)

    今天专门讲一个--CommandMethod.我们都在知道CAD操作要快,必须要熟悉掌握各种命令.在Lisp开发中,在函数后C:即可添加一个命令,非常方法,在.NET API也可以非常方便的设置命令, ...

  9. java.util.HashMap 解析

    HashMap 是我们经常使用的一种数据结构.工作中会经常用到,面试也会总提到这个数据结构,找工作的时候,”HashTable 和HashMap的区别“被问到过没有? 本文会从原理,JDK源码,项目使 ...

  10. php读取ini(init)文件

    <?php header('content-type:text/html;charset=utf-8'); //读取.init文件 $config_file_path = './config/d ...