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. FIS的安装

    FIS是专为解决前端开发中自动化工具.性能优化.模块化框架.开发规范.代码部署.开发流程等问题的前端工程化构建工具. 最重要的是,它是国产的!还是百度产的~~~亲切吧~~官网:http://fis.b ...

  2. Neural Networks for Machine Learning by Geoffrey Hinton (4)

    一种能够学习家谱关系的简单神经网络 血缘一共同拥有12种关系: son, daughter, nephew, niece, father, mother, uncle, aunt, brother, ...

  3. 【OpenGL】用OpenGL shader实现将YUV(YUV420,YV12)转RGB-(直接调用GPU实现,纯硬件方式,效率高)

    这段时间一直在搞视频格式的转换问题,终于最近将一个图片的YUV格式转RGB格式转换成功了.下面就来介绍一下: 由于我的工程是在vs2008中的,其中包含一些相关头文件和库,所以下面只是列出部分核心代码 ...

  4. wamp php.ini 配置的坑

    wampserver是windows平台下一键部署PHP+apache+MySQL的开发环境安装包,非常方便,但修改php.ini时需要注意,wamp目录下有两个php.ini, 第一个是apatch ...

  5. 让你的Photoshop编辑制作ICO格式图标文件(ICOFormat支持图标文件插件)

    相信非常多制图的朋友都喜欢用PS,可是你能用Photoshop保存为ICO格式图标文件吗?默认肯定不行.不知道是什么原因,大名鼎鼎的图像编辑软件Adobe Photoshop一直不支持导入导出ico格 ...

  6. .net framework中重新注册IIS

    要为 ASP.NET 修复 IIS 映射,请按照下列步骤执行操作:运行 Aspnet_regiis.exe 实用工具:单击“开始”,然后单击“运行”.在“打开”文本框中,键入 cmd,然后按 ENTE ...

  7. kubernetes 部署SonarQube 7.1 关联LDAP

    之前有写过一篇如何在kubernetes上部署SonarQube的文档, 然后由于客户的需求,需要SonarQube关联LDAP的用户, 于是今天花了半天时间研究了以下如何在原有的基础上安装LDAP插 ...

  8. 网页制作,网站制作中put和get的区别

    Http定义了与服务器交互的不同方法,最基本的方法有4种,分别是GET,POST,PUT,DELETE.URL全称是资源描述符,我们可以这样认为:一个URL地址,它用于描述一个网络上的资源,而HTTP ...

  9. PHP面向对象之接口 (interface)

    1.使用接口,接口中指定了某个类必须实现的某些方法,这些方法都是空的(不需要定义这些方法的具体内容) 2. 要实现一个接口用关键字implements,类中必须包含接口中所有的方法,否则会出现一个致命 ...

  10. js中,三元运算的简单应用(?:)

    js中,三元运算的简单应用: var sinOrMul = ""; sinOrMul =(subType=="single")?("<span ...