24-Concurrent-Programming
第二十四章 并发编程
爱丽丝
: “我可不想到疯子中间去”猫咪
: “啊,那没辙了,我们这都是疯子。我疯了,你也疯了”爱丽丝
: “你怎么知道我疯了”。猫咪
: “你一定是疯了,否则你就不会来这儿” ——爱丽丝梦游仙境 第6 章。
在本章之前,我们惯用一种简单顺序的叙事方式来编程,有点类似文学中的意识流:第一件事发生了,然后是第二件,第三件……总之,我们完全掌握着事情发生的进展和顺序。如果我们将一个值设置为
现在,我们来到了陌生的并发世界,在这里这样的结果一点都不奇怪。你原来相信的一切都不再可靠。原有的规则可能生效也可能失效。更可能的是原有的规则只会在某些情况下生效。我们只有完全了解这些情况,才能决定我们处理事情的方式。
比如,我们正常的生活的世界是遵循经典牛顿力学的。物体具有质量:会坠落并且转移动能。电线有电阻,光沿直线传播。假如我们进入到极小、极大、极冷或者极热(那些我们无法生存的世界
假设我们处在多条故事线并行的间谍小说里,非单一意识流地叙事:第一个间谍在岩石底留下了微缩胶片。当第二个间谍来取时,胶片可能已被第三个间谍拿走。小说并没有交代此处的细节。所以直到故事结尾,我们都没搞清楚到底发生了什么。
构建并发程序好比玩搭积木游戏。每拉出一块放在塔顶时都有崩塌的可能。每个积木塔和应用程序都是独一无二的,有着自己的作用。你在某个系统构建中学到的知识并不一定适用于下一个系统。
本章是对并发概念最基本的介绍。虽然我们用到了现代的
更多繁琐和底层的细节,请参阅附录:并发底层原理。要进一步深入这个领域,你还必须阅读
术语问题
术语“并发”
- 并发是关于正确有效地控制对共享资源的访问。
- 并行是使用额外的资源来更快地产生结果。
这些定义很好,但是我们已有几十年混乱使用和抗拒解决此问题的历史了。一般来说,当人们使用“并发”这个词时,他们的意思是“所有的一切”。事实上,我自己也经常陷入这样的想法。在大多数书籍中,包括
“并发”通常表示
还有另一种方式,围绕”缓慢“出现的情况写下定义:
并发
同时完成多任务。无需等待当前任务完成即可执行其他任务
并行
同时在多个位置完成多任务。这解决了所谓的
上面的定义说明了这两个术语令人困惑的原因:两者的核心都是“同时完成多个任务”,不过并行增加了跨多个处理器的分布。更重要的是,它们可以解决不同类型的问题:并行可能对解决
这两个概念混合在一起的一个主要原因是包括
我们甚至可以尝试以更细的粒度去进行定义(然而这并不是标准化的术语
-
纯并发:仍然在单个
CPU 上运行任务。纯并发系统比时序系统更快地产生结果,但是它的运行速度不会因为处理器的增加而变得更快。 -
并发
- 并行:使用并发技术,结果程序可以利用多处理器更快地产生结果。 -
并行
- 并发:使用并行编程技术编写,即使只有一个处理器,结果程序仍然可以运行(Java 8 Streams 就是一个很好的例子 ) 。 -
纯并行:只有多个处理器的情况下才能运行。
在某些情况下,这是一个有效的分类法。
支持并发性的语言和库似乎是抽象泄露(Leaky Abstraction一词的完美候选。抽象的目标是“抽象”掉那些对手头的想法不重要的部分,以屏蔽不必要的细节所带来的影响。如果抽象发生泄露,那么即使费很大功夫去隐藏它们,这些细枝末节也总会不断凸显出自己是重要的。
于是我开始怀疑是否真的有高度地抽象。因为当编写这类程序时,底层的系统、工具,甚至是关于
你可能会认为纯函数式语言没有这些限制。实际上,纯函数式语言的确解决了大量并发问题。如果你正在解决一个困难的并发问题,可以考虑用纯函数语言编写这个部分。但是,如果你编写一个使用队列的系统,例如,如果该系统没有被合理地调优,并且输入速率也没有被正确地估计或限制(在不同的情况下,限制意味着具有不同的影响的不同东西
并发的新定义
几十年来,我一直在努力解决各种形式的并发问题,其中一个最大的挑战是简洁的定义它。在撰写本章的过程中,我终于有了这样的洞察力,我将其定义为:
并发性是一系列专注于减少等待的性能技术
这实际上是一个相当复杂的表述,所以我将其分解:
- 这是一个集合:包含许多不同的方法来解决这个问题。因为技术差异很大,这是使定义并发性如此具有挑战性的问题之一。
- 这些是性能技术:就是这样。并发的关键点在于让你的程序运行得更快。在
Java 中,并发是非常棘手和困难的,所以绝对不要使用它,除非你有一个重大的性能问题- 即使这样,使用最简单的方法产生你需要的性能,因为并发很快变得难以管理。 - “减少等待”部分很重要而且微妙。无论(例如)你的程序运行在多少个处理器上,你只能在等待发生时产生效益。如果你发起
I/O 请求并立即获得结果,没有延迟,因此无需改进。如果你在多个处理器上运行多个任务,并且每个处理器都以满容量运行,并且没有任务需要等待其他任务,那么尝试提高吞吐量是没有意义的。并发的唯一机会是程序的某些部分被迫等待。等待会以多种形式出现- 这解释了为什么存在多种不同的并发方法。
值得强调的是,这个定义的有效性取决于“等待”这个词。如果没有什么可以等待,那就没有机会去加速。如果有什么东西在等待,那么就会有很多方法可以加快速度,这取决于多种因素,包括系统运行的配置,你要解决的问题类型以及其他许多问题。
并发的超能力
想象一下,你置身于一部科幻电影。你必须在一栋大楼中找到一个东西,它被小心而巧妙地隐藏在大楼千万个房间中的一间。你进入大楼,沿着走廊走下去。走廊是分开的。
一个人完成这项任务要花上一百辈子的时间。
现在假设你有一个奇怪的超能力。你可以将自己一分为二,然后在继续前进的同时将另一半送到另一个走廊。每当你在走廊或楼梯上遇到分隔到下一层时,你都会重复这个分裂的技巧。最终,整个建筑中的每个走廊的终点都有一个你。
每个走廊都有一千个房间。此时你的超能力变得弱了一点,你只能克隆
一旦克隆体进入房间,它必须搜索房间的每个角落。这时它切换到了第二种超能力。它分裂成了一百万个纳米机器人,每个机器人都会飞到或爬到房间里一些看不见的地方。你不需要了解这种功能
我很想说
以下是其中一个泄露:在理想的世界中,每次克隆自己时,也会复制一个物理处理器来运行克隆搜索者。这当然是不现实的——实际上,你的机器上一般只有
让我们在科幻电影中改变一些东西。现在当每个克隆搜索者最终到达一扇门时,他们必须敲门并等到有人开门。如果每个搜索者都有一个处理器核心,这没有问题——只是空闲等待直到有人开门。但是如果我们只有
许多模型能够有效地隐藏处理器的数量,允许你假装有很多个处理器。但在某些情况下,当你必须明确知道处理器数量以便于工作的时候,这些模型就会失效。
最大的影响之一取决于是使用单核处理器还是多核处理器。如果你只有单核处理器,那么任务切换的成本也由该核心承担,将并发技术应用于你的系统会使它运行得更慢。
这可能会让你以为,在单核处理器的情况下,编写并发代码是没有意义的。然而,有些情况下,并发模型会产生更简单的代码,光是为了这个目的就值得舍弃一些性能。
在克隆体敲门等待的情况下,即使单核处理器系统也能从并发中受益,因为它可以从等待(阻塞)的任务切换到准备运行的任务。但是如果所有任务都可以一直运行那么切换的成本反而会使任务变慢,在这种情况下,并发只在如果你有多个处理器的情况下有意义。
假设你正在尝试破解某种密码,在同一时间内参与破解的线程越多,你越快得到答案的可能性就越大。每个线程都能持续使用你所分配的处理器时间,在这种情况下(
在接听电话的客户服务部门,你只有一定数量的员工,但是你的部门可能会收到很多电话。这些员工(处理器)一次只能接听一个电话直到打完,此时其它打来的电话必须排队等待。
在“鞋匠和精灵”的童话故事中,鞋匠有很多工作要做,当他睡着时,出现了一群精灵来为他制作鞋子。这里的工作是分布式的,但即使使用大量的物理处理器,在制造鞋子的某些部件时也会产生限制——例如,如果鞋底的制作时间最长,这就限制了鞋子的制作速度,这也会改变你设计解决方案的方式。
因此,你要解决的问题驱动了方案的设计。将一个问题分解成“独立运行”的子任务,这是一种美好的抽象,然后就是实际发生的现实:物理现实不断干扰和动摇这个抽象。
这只是问题的一部分:考虑一个制作蛋糕的工厂。我们以某种方式把制作蛋糕的任务分给了工人们,现在是时候让工人把蛋糕放在盒子里了。那里有一个准备存放蛋糕的盒子。但是在一个工人把蛋糕放进盒子之前,另一个工人就冲过去,把蛋糕放进盒子里,砰!这两个蛋糕撞到一起砸坏了。这是常见的“共享内存”问题,会产生所谓的竞态条件(race condition
当“同时”执行的任务相互干扰时,就会出现问题。这可能以一种微妙而偶然的方式发生,因此可以说并发是“可以论证的确定性,但实际上是不确定性的”。也就是说,假设你很小心地编写并发程序,而且通过了代码检查可以正确运行。然而实际上,我们编写的并发程序大部分情况下都能正常运行,但是在一些特定情况下会失败。这些情况可能永远不会发生,或者在你在测试期间几乎很难发现它们。实际上,编写测试代码通常无法为并发程序生成故障条件。由此产生的失败只会偶尔发生,因此它们以客户投诉的形式出现。这是学习并发中最强有力的论点之一:如果你忽略它,你可能会受伤。
因此,并发似乎充满了危险,如果这让你有点害怕,这可能是一件好事。尽管
并发为速度而生
在听说并发编程的问题之后,你可能会想知道它是否值得这么麻烦。答案是“不,除非你的程序运行速度不够快
速度问题一开始听起来很简单:如果你想要一个程序运行得更快,将其分解为多个部分,并在单独的处理器上运行每个部分。随着我们提高时钟速度的能力耗尽(至少对传统芯片而言
对于多处理器机器,可以在这些处理器之间分配多个任务,这可以显著提高吞吐量。强大的多处理器
但是,并发通常可以提高在单处理器上运行的程序的性能。这听起来有点违反直觉。如果你仔细想想,由于上下文切换的成本增加(从一个任务切换到另一个任务
使这个问题变得有些不同的是阻塞。如果程序中的某个任务由于程序控制之外的某种情况而无法继续(通常是
单处理器系统中性能改进的一个常见例子是事件驱动编程,特别是用户界面编程。考虑一个程序执行一些耗时操作,最终忽略用户输入导致无响应。如果你有一个“退出”按钮,你不想在你编写的每段代码中都检查它的状态(轮询
实现并发的一种简单方式是使用操作系统级别的进程。与线程不同,进程是在其自己的地址空间中运行的独立程序。进程的优势在于,因为操作系统通常将一个进程与另一个进程隔离,因此它们不会相互干扰,这使得进程编程相对容易。相比之下,线程之间会共享内存和
有些人甚至提倡将进程作为唯一合理的并发实现方式1,但遗憾的是,通常存在数量和开销方面的限制,从而阻止了进程在并发范围内的适用性(最终你会习惯标准的并发限制
一些编程语言旨在将并发任务彼此隔离。这些通常被称为函数式语言,其中每个函数调用不产生副作用(不会干扰到其它函数
并发会带来各种成本,包括复杂性成本,但可以被程序设计、资源平衡和用户便利性方面的改进所抵消。通常,并发性使你能够创建更低耦合的设计;另一方面,你必须特别关注那些使用了并发操作的代码。
Java 并发的四句格言
在经历了多年
1. 不要用它(避免使用并发)
2. 没有什么是真的,一切可能都有问题
3. 仅仅是它能运行,并不意味着它没有问题
4. 你必须理解它(逃不掉并发)
这些格言专门针对
1. 不要用它
(而且不要自己去实现它)
避免陷入并发所带来的玄奥问题的最简单方法就是不要用它。尽管尝试一些简单的东西可能很诱人,也似乎足够安全,但是陷阱却是无穷且微妙的。如果你能避免使用它,你的生活将会轻松得多。
使用并发唯一的正当理由是速度。如果你的程序运行速度不够快——这里要小心,因为仅仅想让它运行得更快不是正当理由——应该首先用一个分析器(参见代码校验章中分析和优化)来发现你是否可以执行其他一些优化。
如果你被迫使用并发,请采取最简单,最安全的方法来解决问题。使用知名的库并尽可能少地自己编写代码。对于并发,就没有“太简单了”——自作聪明是你的敌人。
2. 没有什么是真的,一切可能都有问题
不使用并发编程,你已经预料到你的世界具有确定的顺序和一致性。对于变量赋值这样简单的操作,很明显它应该总是能够正常工作。
在并发领域,有些事情可能是真的而有些事情却不是,以至于你必须假设没有什么是真的。你必须质疑一切。即使将变量设置为某个值也可能不会按预期的方式工作,事情从这里开始迅速恶化。我已经熟悉了这样一种感觉:我认为应该明显奏效的东西,实际上却行不通。
在非并发编程中你可以忽略的各种事情,在并发下突然变得很重要。例如,你必须了解处理器缓存以及保持本地缓存与主内存一致的问题,你必须理解对象构造的深层复杂性,这样你的构造函数就不会意外地暴露数据,以致于被其它线程更改。这样的例子不胜枚举。
虽然这些主题过于复杂,无法在本章中给你提供专业知识(同样,请参见
3. 仅仅是它能运行,并不意味着它没有问题
我们很容易编写出一个看似正常实则有问题的并发程序,而且问题只有在极少的情况下才会显现出来——在程序部署后不可避免地会成为用户问题(投诉
- 你不能验证出并发程序是正确的,你只能(有时)验证出它是不正确的。
- 大多数情况下你甚至没办法验证:如果它出问题了,你可能无法检测到它。
- 你通常无法编写有用的测试,因此你必须依靠代码检查和对并发的深入了解来发现错误。
- 即使是有效的程序也只能在其设计参数下工作。当超出这些设计参数时,大多数并发程序会以某种方式失败。
在其他
我自己的经验是,无论你是多么确定你的代码是线程安全的,它都可能是有问题的。你可以很容易地了解所有的问题,然后几个月或几年后你会发现一些概念,让你意识到你编写的大多数代码实际上都容易受到并发
在
4. 你必须理解它
在格言
这是一种理性的反应。你可能知道其他更好地被设计用于构建并发程序的编程语言——甚至是在
唉,你不能轻易逃脱:
- 即使你从未显示地创建一个线程,你使用的框架也可能——例如,
Swing 图形用户界面(GUI)库,或者像Timer 类(计时器)那样简单的东西。 - 最糟糕的是:当你创建组件时,必须假设这些组件可能会在多线程环境中重用。即使你的解决方案是放弃并声明你的组件是“非线程安全的”,你仍然必须充分了解这样一个语句的重要性及其含义。
人们有时会认为并发对于介绍语言的书来说太高级了,因此不适合放在其中。他们认为并发是一个独立的主题,并且对于少数出现在日常的程序设计中的情况(例如图形用户界面
唉,如果是这样就好了。遗憾的是,对于线程何时出现在
残酷的真相
当人类开始烹饪他们的食物时,他们大大减少了他们的身体分解和消化食物所需的能量。烹饪创造了一个“外化的胃”,从而释放出能量去发展其他的能力。火的使用促成了文明。
我们现在通过计算机和网络技术创造了一个“外化大脑”,开始了第二次基本转变。虽然我们只是触及表面,但已经引发了其他转变,例如设计生物机制的能力,并且已经看到文化演变的显著加速(过去,人们通过旅游进行文化交流,但现在他们开始在互联网上做这件事
有了这种根本性的人类变化,看到许多破坏和失败的实验并不令人惊讶。实际上,进化依赖于无数的实验,其中大多数都失败了。这些实验是向前发展的必要条件。
紧迫感来自互联网的最初兴起。它似乎是一场比赛,第一个通过起跑线的人将“获胜”(事实上,Java,
Turing completeness 是不足够的
热情使原始
线程包含在
不幸的是,为了在更高级别的语言中获得并发性,所有语言功能都会受到影响,包括最基本的功能,例如标识符代表可变值。在简化并发编程中,所有函数和方法中为了保持事物不变和防止副作用都要做出巨大的改变(这些是纯函数式编程语言的基础
为了获得正确的并发性,语言功能必须从头开始设计并考虑并发性。木已成舟;
尽管有这些基本的不可修复的缺陷,但令人印象深刻的是它已经走了这么远。
这些改进非常有用,我们将在本章重点介绍并行流和
本章其余部分
这是我们将在本章的其余部分介绍的内容。请记住,本章的重点是使用最新的高级
- Parallel Streams(并行流)
到目前为止,我已经强调了
Java 8 Streams 提供的改进语法。现在该语法(作为一个粉丝,我希望)会使你感到舒适,你可以获得额外的好处:你可以通过简单地将parallel() 添加到表达式来并行化流。这是一种简单,强大,坦率地说是利用多处理器的惊人方式
添加
- 创建和运行任务
任务是一段可以独立运行的代码。为了解释创建和运行任务的一些基础知识,本节介绍了一种比并行流或
CompletableFutures 更简单的机制:Executor。执行者管理一些低级Thread 对象(Java 中最原始的并发形式) 。你创建一个任务,然后将其交给Executor 去运行。
有多种类型的
- 终止长时间运行的任务
任务独立运行,因此需要一种机制来关闭它们。典型的方法使用了一个标志,这引入了共享内存的问题,我们将使用
Java 的“Atomic”库来回避它。 - Completable Futures
当你将衣服带到干洗店时,他们会给你一张收据。你继续完成其他任务,当你的衣服洗干净时你可以把它取走。收据是你与干洗店在后台执行的任务的连接。这是
Java 5 中引入的Future 的方法。
- 死锁
某些任务必须去等待
- 阻塞来获得其他任务的结果。被阻止的任务有可能等待另一个被阻止的任务,另一个被阻止的任务也在等待其他任务,等等。如果被阻止的任务链循环到第一个,没有人可以取得任何进展,你就会陷入死锁。
如果在运行程序时没有立即出现死锁,则会出现最大的问题。你的系统可能容易出现死锁,并且只会在某些条件下死锁。程序可能在某个平台上(例如在你的开发机器)运行正常,但是当你将其部署到不同的硬件时会开始死锁。
死锁通常源于细微的编程错误
- 努力,复杂,成本
我们将通过模拟创建披萨的过程完成本章,首先使用并行流实现它,然后是
并行流
.parallel()
就会产生魔法般的结果,流中的所有内容都作为一组并行任务运行。如果你的代码是使用
例如,考虑来自
// concurrent/ParallelPrime.java
import java.util.*;
import java.util.stream.*;
import static java.util.stream.LongStream.*;
import java.io.*;
import java.nio.file.*;
import onjava.Timer;
public class ParallelPrime {
static final int COUNT = 100_000;
public static boolean isPrime(long n){
return rangeClosed(2, (long)Math.sqrt(n)).noneMatch(i -> n % i == 0);
}
public static void main(String[] args)
throws IOException {
Timer timer = new Timer();
List<String> primes =
iterate(2, i -> i + 1)
.parallel() // [1]
.filter(ParallelPrime::isPrime)
.limit(COUNT)
.mapToObj(Long::toString)
.collect(Collectors.toList());
System.out.println(timer.duration());
Files.write(Paths.get("primes.txt"), primes, StandardOpenOption.CREATE);
}
}
输出结果:
Output:
1224
请注意,这不是微基准测试,因为我们计时整个程序。我们将数据保存在磁盘上以防止编译器过激的优化
当我注释掉
并行流似乎是一个甜蜜的交易。你所需要做的就是将编程问题转换为流,然后插入
parallel() 不是灵丹妙药
作为对流和并行流的不确定性的探索,让我们看一个看似简单的问题:对增长的数字序列进行求和。事实证明有大量的方式去实现它,并且我将冒险用计时器将它们进行比较
我将从一个计时方法
请注意,一切都必须严格使用
所有关于时间和内存的数字和讨论都是指“我的机器”。你的经历可能会有所不同。
// concurrent/Summing.java
import java.util.stream.*;
import java.util.function.*;
import onjava.Timer;
public class Summing {
static void timeTest(String id, long checkValue, LongSupplier operation){
System.out.print(id + ": ");
Timer timer = new Timer();
long result = operation.getAsLong();
if(result == checkValue)
System.out.println(timer.duration() + "ms");
else
System.out.format("result: %d%ncheckValue: %d%n", result, checkValue);
}
public static final int SZ = 100_000_000;
// This even works:
// public static final int SZ = 1_000_000_000;
public static final long CHECK = (long)SZ * ((long)SZ + 1)/2; // Gauss's formula
public static void main(String[] args){
System.out.println(CHECK);
timeTest("Sum Stream", CHECK, () ->
LongStream.rangeClosed(0, SZ).sum());
timeTest("Sum Stream Parallel", CHECK, () ->
LongStream.rangeClosed(0, SZ).parallel().sum());
timeTest("Sum Iterated", CHECK, () ->
LongStream.iterate(0, i -> i + 1)
.limit(SZ+1).sum());
// Slower & runs out of memory above 1_000_000:
// timeTest("Sum Iterated Parallel", CHECK, () ->
// LongStream.iterate(0, i -> i + 1)
// .parallel()
// .limit(SZ+1).sum());
}
}
输出结果:
5000000050000000
Sum Stream: 167ms
Sum Stream Parallel: 46ms
Sum Iterated: 284ms
如果使用
- 流并行性将输入数据分成多个部分,因此算法可以应用于那些单独的部分。
- 数组分割成本低,分割均匀且对分割的大小有着完美的掌控。
- 链表没有这些属性
; “拆分”一个链表仅仅意味着把它分成“第一元素”和“其余元素”,这相对无用。 - 无状态生成器的行为类似于数组
; 上面使用的range() 就是无状态的。 - 迭代生成器的行为类似于链表
; iterate() 是一个迭代生成器。
现在让我们尝试通过在数组中填充值并对数组求和来解决问题。因为数组只分配了一次,所以我们不太可能遇到垃圾收集时序问题。
首先我们将尝试一个充满原始
// concurrent/Summing2.java
// {ExcludeFromTravisCI}import java.util.*;
public class Summing2 {
static long basicSum(long[] ia) {
long sum = 0;
int size = ia.length;
for(int i = 0; i < size; i++)
sum += ia[i];return sum;
}
// Approximate largest value of SZ before
// running out of memory on mymachine:
public static final int SZ = 20_000_000;
public static final long CHECK = (long)SZ * ((long)SZ + 1)/2;
public static void main(String[] args) {
System.out.println(CHECK);
long[] la = newlong[SZ+1];
Arrays.parallelSetAll(la, i -> i);
Summing.timeTest("Array Stream Sum", CHECK, () ->
Arrays.stream(la).sum());
Summing.timeTest("Parallel", CHECK, () ->
Arrays.stream(la).parallel().sum());
Summing.timeTest("Basic Sum", CHECK, () ->
basicSum(la));// Destructive summation:
Summing.timeTest("parallelPrefix", CHECK, () -> {
Arrays.parallelPrefix(la, Long::sum);
return la[la.length - 1];
});
}
}
输出结果:
200000010000000
Array Stream
Sum: 104ms
Parallel: 81ms
Basic Sum: 106ms
parallelPrefix: 265ms
第一个限制是内存大小;因为数组是预先分配的,所以我们不能创建几乎与以前版本一样大的任何东西。并行化可以加快速度,甚至比使用
最后,考虑使用包装类
// concurrent/Summing3.java
// {ExcludeFromTravisCI}
import java.util.*;
public class Summing3 {
static long basicSum(Long[] ia) {
long sum = 0;
int size = ia.length;
for(int i = 0; i < size; i++)
sum += ia[i];
return sum;
}
// Approximate largest value of SZ before
// running out of memory on my machine:
public static final int SZ = 10_000_000;
public static final long CHECK = (long)SZ * ((long)SZ + 1)/2;
public static void main(String[] args) {
System.out.println(CHECK);
Long[] aL = newLong[SZ+1];
Arrays.parallelSetAll(aL, i -> (long)i);
Summing.timeTest("Long Array Stream Reduce", CHECK, () ->
Arrays.stream(aL).reduce(0L, Long::sum));
Summing.timeTest("Long Basic Sum", CHECK, () ->
basicSum(aL));
// Destructive summation:
Summing.timeTest("Long parallelPrefix",CHECK, ()-> {
Arrays.parallelPrefix(aL, Long::sum);
return aL[aL.length - 1];
});
}
}
输出结果:
50000005000000
Long Array
Stream Reduce: 1038ms
Long Basic
Sum: 21ms
Long parallelPrefix: 3616ms
现在可用的内存量大约减半,并且所有情况下所需的时间都会很长,除了
我将
// concurrent/Summing4.java
// {ExcludeFromTravisCI}
import java.util.*;
public class Summing4 {
public static void main(String[] args) {
System.out.println(Summing3.CHECK);
Long[] aL = newLong[Summing3.SZ+1];
Arrays.parallelSetAll(aL, i -> (long)i);
Summing.timeTest("Long Parallel",
Summing3.CHECK, () ->
Arrays.stream(aL)
.parallel()
.reduce(0L,Long::sum));
}
}
输出结果:
50000005000000
Long Parallel: 1014ms
它比非
导致时间增加的一个重要原因是处理器内存缓存。使用
使用
这些示例使用不同的
为了进行时间比较,以下是
Sum Stream: 69msSum Stream Parallel: 18msSum Iterated: 277ms Array Stream Sum: 57ms Parallel: 14ms Basic Sum: 16ms parallelPrefix: 28ms Long Array Stream Reduce: 1046ms Long Basic Sum: 21ms Long parallelPrefix: 3287ms Long Parallel: 1008ms
虽然
parallel()/limit() 交点
使用
// concurrent/CollectionIntoStream.java
import onjava.*;
import java.util.*;
import java.util.stream.*;
public class CollectionIntoStream {
public static void main(String[] args) {
List<String> strings = Stream.generate(new Rand.String(5))
.limit(10)
.collect(Collectors.toList());
strings.forEach(System.out::println);
// Convert to a Stream for many more options:
String result = strings.stream()
.map(String::toUpperCase)
.map(s -> s.substring(2))
.reduce(":", (s1, s2) -> s1 + s2);
System.out.println(result);
}
}
输出结果:
btpen
pccux
szgvg
meinn
eeloz
tdvew
cippc
ygpoa
lkljl
bynxt
:PENCUXGVGINNLOZVEWPPCPOALJLNXT
在许多情况下,只在集合上调用
// concurrent/ParallelStreamPuzzle.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class ParallelStreamPuzzle {
static class IntGenerator
implements Supplier<Integer> {
private int current = 0;
@Override
public Integer get() {
return current++;
}
}
public static void main(String[] args) {
List<Integer> x = Stream.generate(new IntGenerator())
.limit(10)
.parallel() // [1]
.collect(Collectors.toList());
System.out.println(x);
}
}
/* Output:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
*/
如果
为了看到它,我们将添加一些仪器。由于我们正在处理线程,因此我们必须将任何跟踪信息捕获到并发数据结构中。在这里我使用
// concurrent/ParallelStreamPuzzle2.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.nio.file.*;
public class ParallelStreamPuzzle2 {
public static final Deque<String> TRACE =
new ConcurrentLinkedDeque<>();
static class
IntGenerator implements Supplier<Integer> {
private AtomicInteger current =
new AtomicInteger();
@Override
public Integer get() {
TRACE.add(current.get() + ": " +Thread.currentThread().getName());
return current.getAndIncrement();
}
}
public static void main(String[] args) throws Exception {
List<Integer> x = Stream.generate(newIntGenerator())
.limit(10)
.parallel()
.collect(Collectors.toList());
System.out.println(x);
Files.write(Paths.get("PSP2.txt"), TRACE);
}
}
输出结果:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
在查看
0: main
1: ForkJoinPool.commonPool-worker-1
2: ForkJoinPool.commonPool-worker-2
3: ForkJoinPool.commonPool-worker-2
4: ForkJoinPool.commonPool-worker-1
5: ForkJoinPool.commonPool-worker-1
6: ForkJoinPool.commonPool-worker-1
7: ForkJoinPool.commonPool-worker-1
8: ForkJoinPool.commonPool-worker-4
9: ForkJoinPool.commonPool-worker-4
10: ForkJoinPool.commonPool-worker-4
11: main
12: main
13: main
14: main
15: main...10
17: ForkJoinPool.commonPool-worker-110
18: ForkJoinPool.commonPool-worker-610
19: ForkJoinPool.commonPool-worker-610
20: ForkJoinPool.commonPool-worker-110
21: ForkJoinPool.commonPool-worker-110
22: ForkJoinPool.commonPool-worker-110
23: ForkJoinPool.commonPool-worker-1
这个块大小似乎是内部实现的一部分(尝试使用limit()
的不同参数来查看不同的块大小parallel()
与limit()
结合使用可以预取一串值,作为流输出。
试着想象一下这里发生了什么:一个流抽象出无限序列,按需生成。当你要求它并行产生流时,你要求所有这些线程尽可能地调用get()
。添加limit()
,你说“只需要这些parallel()
与limit()
结合使用时,这种方法可能对你正在解决的问题有效。但是当你这样做时,你必须明白。这是一个仅限专家的功能,而不是要争辩说“
什么是更合理的方法来解决问题?好吧,如果你想生成一个
// concurrent/ParallelStreamPuzzle3.java
// {VisuallyInspectOutput}
import java.util.*;
import java.util.stream.*;
public class ParallelStreamPuzzle3 {
public static void main(String[] args) {
List<Integer> x = IntStream.range(0, 30)
.peek(e -> System.out.println(e + ": " +Thread.currentThread()
.getName()))
.limit(10)
.parallel()
.boxed()
.collect(Collectors.toList());
System.out.println(x);
}
}
输出结果:
8: main
6: ForkJoinPool.commonPool-worker-5
3: ForkJoinPool.commonPool-worker-7
5: ForkJoinPool.commonPool-worker-5
1: ForkJoinPool.commonPool-worker-3
2: ForkJoinPool.commonPool-worker-6
4: ForkJoinPool.commonPool-worker-1
0: ForkJoinPool.commonPool-worker-4
7: ForkJoinPool.commonPool-worker-1
9: ForkJoinPool.commonPool-worker-2
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
为了表明
你还可以看到
现在我们得到多个线程产生不同的值,但它只产生
它更快吗?一个更好的问题是:什么时候开始有意义?当然不是这么小的一套;上下文切换的代价远远超过并行性的任何加速。很难想象什么时候用并行生成一个简单的数字序列会有意义。如果你要生成的东西需要很高的成本,它可能有意义
- 并行流只看起来很容易
实际上,在许多情况下,并行流确实可以毫不费力地更快地产生结果。但正如你所见,仅仅将
创建和运行任务
如果无法通过并行流实现并发,则必须创建并运行自己的任务。稍后你将看到运行任务的理想
- Tasks and Executors
在
创建所有这些线程的开销变得非常重要,现在不鼓励采用手动操作方法。在
首先,我们将创建一个几乎不执行任务的任务。它“sleep”(暂停执行)
// concurrent/NapTask.java
import onjava.Nap;
public class NapTask implements Runnable {
final int id;
public NapTask(int id) {
this.id = id;
}
@Override
public void run() {
new Nap(0.1);// Seconds
System.out.println(this + " "+
Thread.currentThread().getName());
}
@Override
public String toString() {
return"NapTask[" + id + "]";
}
}
这只是一个
// onjava/Nap.java
package onjava;
import java.util.concurrent.*;
public class Nap {
public Nap(double t) { // Seconds
try {
TimeUnit.MILLISECONDS.sleep((int)(1000 * t));
} catch(InterruptedException e){
throw new RuntimeException(e);
}
}
public Nap(double t, String msg) {
this(t);
System.out.println(msg);
}
}
为了消除异常处理的视觉干扰,这被定义为实用程序。第二个构造函数在超时时显示一条消息
对
你可以看到
要执行任务,我们将从最简单的方法–
//concurrent/SingleThreadExecutor.java
import java.util.concurrent.*;
import java.util.stream.*;
import onjava.*;
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService exec =
Executors.newSingleThreadExecutor();
IntStream.range(0, 10)
.mapToObj(NapTask::new)
.forEach(exec::execute);
System.out.println("All tasks submitted");
exec.shutdown();
while(!exec.isTerminated()) {
System.out.println(
Thread.currentThread().getName()+
" awaiting termination");
new Nap(0.1);
}
}
}
输出结果:
All tasks submitted
main awaiting termination
main awaiting termination
NapTask[0] pool-1-thread-1
main awaiting termination
NapTask[1] pool-1-thread-1
main awaiting termination
NapTask[2] pool-1-thread-1
main awaiting termination
NapTask[3] pool-1-thread-1
main awaiting termination
NapTask[4] pool-1-thread-1
main awaiting termination
NapTask[5] pool-1-thread-1
main awaiting termination
NapTask[6] pool-1-thread-1
main awaiting termination
NapTask[7] pool-1-thread-1
main awaiting termination
NapTask[8] pool-1-thread-1
main awaiting termination
NapTask[9] pool-1-thread-1
首先请注意,没有
我创建了十个
请注意,
如果你只是调用
// concurrent/SingleThreadExecutor2.java
import java.util.concurrent.*;
import java.util.stream.*;
public class SingleThreadExecutor2 {
public static void main(String[] args)throws InterruptedException {
ExecutorService exec
=Executors.newSingleThreadExecutor();
IntStream.range(0, 10)
.mapToObj(NapTask::new)
.forEach(exec::execute);
exec.shutdown();
}
}
输出结果:
NapTask[0] pool-1-thread-1
NapTask[1] pool-1-thread-1
NapTask[2] pool-1-thread-1
NapTask[3] pool-1-thread-1
NapTask[4] pool-1-thread-1
NapTask[5] pool-1-thread-1
NapTask[6] pool-1-thread-1
NapTask[7] pool-1-thread-1
NapTask[8] pool-1-thread-1
NapTask[9] pool-1-thread-1
一旦你调用了
// concurrent/MoreTasksAfterShutdown.java
import java.util.concurrent.*;
public class MoreTasksAfterShutdown {
public static void main(String[] args) {
ExecutorService exec
=Executors.newSingleThreadExecutor();
exec.execute(newNapTask(1));
exec.shutdown();
try {
exec.execute(newNapTask(99));
} catch(RejectedExecutionException e) {
System.out.println(e);
}
}
}
输出结果:
java.util.concurrent.RejectedExecutionException: TaskNapTask[99] rejected from java.util.concurrent.ThreadPoolExecutor@4e25154f[Shutting down, pool size = 1,active threads = 1, queued tasks = 0, completed tasks =0]NapTask[1] pool-1-thread-1
- 使用更多线程
使用线程的重点是(几乎总是)更快地完成任务,那么我们为什么要限制自己使用
// concurrent/CachedThreadPool.java
import java.util.concurrent.*;
import java.util.stream.*;
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService exec
=Executors.newCachedThreadPool();
IntStream.range(0, 10)
.mapToObj(NapTask::new)
.forEach(exec::execute);
exec.shutdown();
}
}
输出结果:
NapTask[7] pool-1-thread-8
NapTask[4] pool-1-thread-5
NapTask[1] pool-1-thread-2
NapTask[3] pool-1-thread-4
NapTask[0] pool-1-thread-1
NapTask[8] pool-1-thread-9
NapTask[2] pool-1-thread-3
NapTask[9] pool-1-thread-10
NapTask[6] pool-1-thread-7
NapTask[5] pool-1-thread-6
当你运行这个程序时,你会发现它完成得更快。这是有道理的,每个任务都有自己的线程,所以它们都并行运行,而不是使用相同的线程来顺序运行每个任务。这似乎没毛病,很难理解为什么有人会使用
要理解这个问题,我们需要一个更复杂的任务:
// concurrent/InterferingTask.java
public class InterferingTask implements Runnable {
final int id;
private static Integer val = 0;
public InterferingTask(int id) {
this.id = id;
}
@Override
public void run() {
for(int i = 0; i < 100; i++)
val++;
System.out.println(id + " "+
Thread.currentThread().getName() + " " + val);
}
}
每个任务增加
// concurrent/CachedThreadPool2.java
import java.util.concurrent.*;
import java.util.stream.*;
public class CachedThreadPool2 {
public static void main(String[] args) {
ExecutorService exec
=Executors.newCachedThreadPool();
IntStream.range(0, 10)
.mapToObj(InterferingTask::new)
.forEach(exec::execute);
exec.shutdown();
}
}
输出结果:
0 pool-1-thread-1 200
1 pool-1-thread-2 200
4 pool-1-thread-5 300
5 pool-1-thread-6 400
8 pool-1-thread-9 500
9 pool-1-thread-10 600
2 pool-1-thread-3 700
7 pool-1-thread-8 800
3 pool-1-thread-4 900
6 pool-1-thread-7 1000
输出不是我们所期望的,并且从一次运行到下一次运行会有所不同。问题是所有的任务都试图写入
// concurrent/SingleThreadExecutor3.java
import java.util.concurrent.*;
import java.util.stream.*;
public class SingleThreadExecutor3 {
public static void main(String[] args)throws InterruptedException {
ExecutorService exec
=Executors.newSingleThreadExecutor();
IntStream.range(0, 10)
.mapToObj(InterferingTask::new)
.forEach(exec::execute);
exec.shutdown();
}
}
输出结果:
0 pool-1-thread-1 100
1 pool-1-thread-1 200
2 pool-1-thread-1 300
3 pool-1-thread-1 400
4 pool-1-thread-1 500
5 pool-1-thread-1 600
6 pool-1-thread-1 700
7 pool-1-thread-1 800
8 pool-1-thread-1 900
9 pool-1-thread-1 1000
现在我们每次都得到一致的结果,尽管
- 产生结果
因为
避免竞争条件的最好方法是避免可变的共享状态。我们可以称之为自私的孩子原则:什么都不分享。
使用
// concurrent/CountingTask.java
import java.util.concurrent.*;
public class CountingTask implements Callable<Integer> {
final int id;
public CountingTask(int id) { this.id = id; }
@Override
public Integer call() {
Integer val = 0;
for(int i = 0; i < 100; i++)
val++;
System.out.println(id + " " +
Thread.currentThread().getName() + " " + val);
return val;
}
}
// concurrent/CachedThreadPool3.java
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class CachedThreadPool3 {
public static Integer extractResult(Future<Integer> f) {
try {
return f.get();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args)throws InterruptedException {
ExecutorService exec =
Executors.newCachedThreadPool();
List<CountingTask> tasks =
IntStream.range(0, 10)
.mapToObj(CountingTask::new)
.collect(Collectors.toList());
List<Future<Integer>> futures =
exec.invokeAll(tasks);
Integer sum = futures.stream()
.map(CachedThreadPool3::extractResult)
.reduce(0, Integer::sum);
System.out.println("sum = " + sum);
exec.shutdown();
}
}
输出结果:
1 pool-1-thread-2 100
0 pool-1-thread-1 100
4 pool-1-thread-5 100
5 pool-1-thread-6 100
8 pool-1-thread-9 100
9 pool-1-thread-10 100
2 pool-1-thread-3 100
3 pool-1-thread-4 100
6 pool-1-thread-7 100
7 pool-1-thread-8 100
sum = 1000
只有在所有任务完成后,
// concurrent/Futures.java
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class Futures {
public static void main(String[] args)throws InterruptedException, ExecutionException {
ExecutorService exec
=Executors.newSingleThreadExecutor();
Future<Integer> f =
exec.submit(newCountingTask(99));
System.out.println(f.get()); // [1]
exec.shutdown();
}
}
输出结果:
99 pool-1-thread-1 100
100
[1] 当你的任务在尚未完成的Future 上调用 get() 时,调用会阻塞(等待)直到结果可用。
但这意味着,在
还要注意在
因为当你调用
我们可以使用并行
// concurrent/CountingStream.java
// {VisuallyInspectOutput}
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class CountingStream {
public static void main(String[] args) {
System.out.println(
IntStream.range(0, 10)
.parallel()
.mapToObj(CountingTask::new)
.map(ct -> ct.call())
.reduce(0, Integer::sum));
}
}
输出结果:
1 ForkJoinPool.commonPool-worker-3 100
8 ForkJoinPool.commonPool-worker-2 100
0 ForkJoinPool.commonPool-worker-6 100
2 ForkJoinPool.commonPool-worker-1 100
4 ForkJoinPool.commonPool-worker-5 100
9 ForkJoinPool.commonPool-worker-7 100
6 main 100
7 ForkJoinPool.commonPool-worker-4 100
5 ForkJoinPool.commonPool-worker-2 100
3 ForkJoinPool.commonPool-worker-3 100
1000
这不仅更容易理解,而且我们需要做的就是将 parallel()
插入到其他顺序操作中,然后一切都在同时运行。
Lambda 和方法引用作为任务
在ExecutorService
// concurrent/LambdasAndMethodReferences.java
import java.util.concurrent.*;
class NotRunnable {
public void go() {
System.out.println("NotRunnable");
}
}
class NotCallable {
public Integer get() {
System.out.println("NotCallable");
return 1;
}
}
public class LambdasAndMethodReferences {
public static void main(String[] args)throws InterruptedException {
ExecutorService exec =
Executors.newCachedThreadPool();
exec.submit(() -> System.out.println("Lambda1"));
exec.submit(new NotRunnable()::go);
exec.submit(() -> {
System.out.println("Lambda2");
return 1;
});
exec.submit(new NotCallable()::get);
exec.shutdown();
}
}
输出结果:
Lambda1
NotCallable
NotRunnable
Lambda2
这里,前两个
终止耗时任务
并发程序通常使用长时间运行的任务。可调用任务在完成时返回值
最初的
InterruptedException,因为设计的向后兼容性残留。
任务终止的最佳方法是设置任务周期性检查的标志。然后任务可以通过自己的
以这种方式终止任务听起来很简单:设置任务可以看到的
在研究
// concurrent/QuittableTask.java
import java.util.concurrent.atomic.AtomicBoolean;import onjava.Nap;
public class QuittableTask implements Runnable {
final int id;
public QuittableTask(int id) {
this.id = id;
}
private AtomicBoolean running =
new AtomicBoolean(true);
public void quit() {
running.set(false);
}
@Override
public void run() {
while(running.get()) // [1]
new Nap(0.1);
System.out.print(id + " "); // [2]
}
}
虽然多个任务可以在同一个实例上成功调用
[1]: 只要运行标志为true ,此任务的run() 方法将继续。
需要
作为测试,我们将启动很多
// concurrent/QuittingTasks.java
import java.util.*;
import java.util.stream.*;
import java.util.concurrent.*;
import onjava.Nap;
public class QuittingTasks {
public static final int COUNT = 150;
public static void main(String[] args) {
ExecutorService es =
Executors.newCachedThreadPool();
List<QuittableTask> tasks =
IntStream.range(1, COUNT)
.mapToObj(QuittableTask::new)
.peek(qt -> es.execute(qt))
.collect(Collectors.toList());
new Nap(1);
tasks.forEach(QuittableTask::quit); es.shutdown();
}
}
输出结果:
24 27 31 8 11 7 19 12 16 4 23 3 28 32 15 20 63 60 68 6764 39 47 52 51 55 40 43 48 59 44 56 36 35 71 72 83 10396 92 88 99 100 87 91 79 75 84 76 115 108 112 104 107111 95 80 147 120 127 119 123 144 143 116 132 124 128
136 131 135 139 148 140 2 126 6 5 1 18 129 17 14 13 2122 9 10 30 33 58 37 125 26 34 133 145 78 137 141 138 6274 142 86 65 73 146 70 42 149 121 110 134 105 82 117106 113 122 45 114 118 38 50 29 90 101 89 57 53 94 4161 66 130 69 77 81 85 93 25 102 54 109 98 49 46 97
我使用
CompletableFuture 类
作为介绍,这里是使用
// concurrent/QuittingCompletable.java
import java.util.*;
import java.util.stream.*;
import java.util.concurrent.*;
import onjava.Nap;
public class QuittingCompletable {
public static void main(String[] args) {
List<QuittableTask> tasks =
IntStream.range(1, QuittingTasks.COUNT)
.mapToObj(QuittableTask::new)
.collect(Collectors.toList());
List<CompletableFuture<Void>> cfutures =
tasks.stream()
.map(CompletableFuture::runAsync)
.collect(Collectors.toList());
new Nap(1);
tasks.forEach(QuittableTask::quit);
cfutures.forEach(CompletableFuture::join);
}
}
输出结果:
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 2526 27 28 29 30 31 32 33 34 6 35 4 38 39 40 41 42 43 4445 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 6263 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 8081 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 9899 100 101 102 103 104 105 106 107 108 109 110 111 1121 113 114 116 117 118 119 120 121 122 123 124 125 126127 128 129 130 131 132 133 134 135 136 137 138 139 140141 142 143 144 145 146 147 148 149 5 115 37 36 2 3
任务是一个 List<QuittableTask>
,就像在 QuittingTasks.java
中一样,但是在这个例子中,没有 peek()
将每个 QuittableTask
提交给 ExecutorService
。相反,在创建 cfutures
期间,每个任务都交给 CompletableFuture::runAsync
。这执行 VerifyTask.run()
并返回 CompletableFuture<Void>
。因为 run()
不返回任何内容,所以在这种情况下我只使用 CompletableFuture
调用 join()
来等待它完成。
在本例中需要注意的重要一点是,运行任务不需要使用 ExecutorService
。而是直接交给 CompletableFuture
管理ExectorService
shutdown()
join()
,否则程序将尽快退出,而不必等待任务完成。
这个例子只是一个起点。你很快就会看到 ComplempleFuture
能够做得更多。
基本用法
这是一个带有静态方法
// concurrent/Machina.java
import onjava.Nap;
public class Machina {
public enum State {
START, ONE, TWO, THREE, END;
State step() {
if(equals(END))
return END;
return values()[ordinal() + 1];
}
}
private State state = State.START;
private final int id;
public Machina(int id) {
this.id = id;
}
public static Machina work(Machina m) {
if(!m.state.equals(State.END)){
new Nap(0.1);
m.state = m.state.step();
}
System.out.println(m);
return m;
}
@Override
public String toString() {
return"Machina" + id + ": " + (state.equals(State.END)? "complete" : state);
}
}
这是一个有限状态机,一个微不足道的机器,因为它没有分支……它只是从头到尾遍历一条路径。
// concurrent/CompletedMachina.java
import java.util.concurrent.*;
public class CompletedMachina {
public static void main(String[] args) {
CompletableFuture<Machina> cf =
CompletableFuture.completedFuture(
new Machina(0));
try {
Machina m = cf.get(); // Doesn't block
} catch(InterruptedException |
ExecutionException e) {
throw new RuntimeException(e);
}
}
}
通常,
当我们将
// concurrent/CompletableApply.java
import java.util.concurrent.*;
public class CompletableApply {
public static void main(String[] args) {
CompletableFuture<Machina> cf =
CompletableFuture.completedFuture(
new Machina(0));
CompletableFuture<Machina> cf2 =
cf.thenApply(Machina::work);
CompletableFuture<Machina> cf3 =
cf2.thenApply(Machina::work);
CompletableFuture<Machina> cf4 =
cf3.thenApply(Machina::work);
CompletableFuture<Machina> cf5 =
cf4.thenApply(Machina::work);
}
}
输出结果:
Machina0: ONE
Machina0: TWO
Machina0: THREE
Machina0: complete
thenApply()
应用一个接收输入并产生输出的函数。在本例中,work()
函数产生的类型与它所接收的类型相同 (Machina
CompletableFuture
添加的操作的返回类型都为 Machina
,但是map()
你可以在此处看到有关
我们可以消除中间变量并将操作链接在一起,就像我们使用
// concurrent/CompletableApplyChained.javaimport java.util.concurrent.*;
import onjava.Timer;
public class CompletableApplyChained {
public static void main(String[] args) {
Timer timer = new Timer();
CompletableFuture<Machina> cf =
CompletableFuture.completedFuture(
new Machina(0))
.thenApply(Machina::work)
.thenApply(Machina::work)
.thenApply(Machina::work)
.thenApply(Machina::work);
System.out.println(timer.duration());
}
}
输出结果:
Machina0: ONE
Machina0: TWO
Machina0: THREE
Machina0: complete
514
这里我们还添加了一个 Timer
,它的功能在每一步都显性地增加CompletableFuture
内部 thenApply
带来的额外开销给体现出来了。
ComplempleFutures
旨在支持这些原则。只要你不决定共享数据(共享非常容易导致意外发生)你就可以编写出相对安全的并发程序。
回调 thenApply()
一旦开始一个操作,在完成所有任务之前,不会完成thenApplyAsync()
来实现此目的:
// concurrent/CompletableApplyAsync.java
import java.util.concurrent.*;
import onjava.*;
public class CompletableApplyAsync {
public static void main(String[] args) {
Timer timer = new Timer();
CompletableFuture<Machina> cf =
CompletableFuture.completedFuture(
new Machina(0))
.thenApplyAsync(Machina::work)
.thenApplyAsync(Machina::work)
.thenApplyAsync(Machina::work)
.thenApplyAsync(Machina::work);
System.out.println(timer.duration());
System.out.println(cf.join());
System.out.println(timer.duration());
}
}
输出结果:
116
Machina0: ONE
Machina0: TWO
Machina0:THREE
Machina0: complete
Machina0: complete
552
同步调用cf
的创建现在发生的更快。每次调用 thenApplyAsync()
都会立刻返回,因此可以进行下一次调用,整个调用链路完成速度比以前快得多。
事实上,如果没有回调 cf.join()
方法,程序会在完成其工作之前退出。而 cf.join()
直到main()
进程结束。我们还可以看出本示例大部分时间消耗在 cf.join()
这。
这种“立即返回”的异步能力需要 CompletableFuture
库进行一些秘密(client
无感)工作。特别是,它将你需要的操作链存储为一组回调。当操作的第一个链路(后台操作)完成并返回时,第二个链路(后台操作)必须获取生成的 Machina
并开始工作,以此类推! 但这种异步机制没有我们可以通过程序调用栈控制的普通函数调用序列,它的调用链路顺序会丢失,因此它使用一个函数地址来存储的回调来解决这个问题。
幸运的是,这就是你需要了解的有关回调的全部信息。程序员将这种人为制造的混乱称为CompletableFuture
帮你管理所有回调。 除非你知道你系统中的一些特定逻辑会导致某些改变,或许你更想使用异步调用来实现程序。
- 其他操作
当你查看CompletableFuture
的 Javadoc
时,你会看到它有很多方法,但这个方法的大部分来自不同操作的变体。例如,有 thenApply()
,thenApplyAsync()
和第二种形式的 thenApplyAsync()
,它们使用 Executor
来运行任务Executor
选项
下面的示例展示了所有CompletableFuture
,也不涉及异常
package onjava;
import java.util.concurrent.*;
public class CompletableUtilities {
// Get and show value stored in a CF:
public static void showr(CompletableFuture<?> c) {
try {
System.out.println(c.get());
} catch(InterruptedException
| ExecutionException e) {
throw new RuntimeException(e);
}
}
// For CF operations that have no value:
public static void voidr(CompletableFuture<Void> c) {
try {
c.get(); // Returns void
} catch(InterruptedException
| ExecutionException e) {
throw new RuntimeException(e);
}
}
}
showr()
在 CompletableFuture<Integer>
上调用 get()
,并显示结果,try/catch
两个可能会出现的异常。
voidr()
是 CompletableFuture<Void>
的 showr()
版本,也就是说,CompletableFutures
只为任务完成或失败时显示信息。
为简单起见,下面的 CompletableFutures
只包装整数。cfi()
是一个便利的方法,它把一个整数包装在一个完整的CompletableFuture<Integer>
// concurrent/CompletableOperations.java
import java.util.concurrent.*;
import static onjava.CompletableUtilities.*;
public class CompletableOperations {
static CompletableFuture<Integer> cfi(int i) {
return
CompletableFuture.completedFuture(
Integer.valueOf(i));
}
public static void main(String[] args) {
showr(cfi(1)); // Basic test
voidr(cfi(2).runAsync(() ->
System.out.println("runAsync")));
voidr(cfi(3).thenRunAsync(() ->
System.out.println("thenRunAsync")));
voidr(CompletableFuture.runAsync(() ->
System.out.println("runAsync is static")));
showr(CompletableFuture.supplyAsync(() -> 99));
voidr(cfi(4).thenAcceptAsync(i ->
System.out.println("thenAcceptAsync: " + i)));
showr(cfi(5).thenApplyAsync(i -> i + 42));
showr(cfi(6).thenComposeAsync(i -> cfi(i + 99)));
CompletableFuture<Integer> c = cfi(7);
c.obtrudeValue(111);
showr(c);
showr(cfi(8).toCompletableFuture());
c = new CompletableFuture<>();
c.complete(9);
showr(c);
c = new CompletableFuture<>();
c.cancel(true);
System.out.println("cancelled: " +
c.isCancelled());
System.out.println("completed exceptionally: " +
c.isCompletedExceptionally());
System.out.println("done: " + c.isDone());
System.out.println(c);
c = new CompletableFuture<>();
System.out.println(c.getNow(777));
c = new CompletableFuture<>();
c.thenApplyAsync(i -> i + 42)
.thenApplyAsync(i -> i * 12);
System.out.println("dependents: " +
c.getNumberOfDependents());
c.thenApplyAsync(i -> i / 2);
System.out.println("dependents: " +
c.getNumberOfDependents());
}
}
输出结果 :
1
runAsync
thenRunAsync
runAsync is static
99
thenAcceptAsync: 4
47
105
111
8
9
cancelled: true
completed exceptionally: true
done: true
java.util.concurrent.CompletableFuture@6d311334[Complet ed exceptionally]
777
dependents: 1
dependents: 2
main()
包含一系列可由其int
值引用的测试。cfi(1)
演示了showr()
正常工作。cfi(2)
是调用runAsync()
的示例。由于Runnable
不产生返回值,因此使用了返回CompletableFuture <Void>
的voidr()
方法。- 注意使用
cfi(3)
, thenRunAsync()
效果似乎与 上例 cfi(2)
使用的runAsync()
相同,差异在后续的测试中体现:runAsync()
是一个static
方法,所以你通常不会像cfi(2)
一样调用它。相反你可以在QuittingCompletable.java
中使用它。- 后续测试中表明
supplyAsync()
也是静态方法,区别在于它需要一个Supplier
而不是Runnable
, 并产生一个CompletableFuture<Integer>
而不是CompletableFuture<Void>
。
then
系列方法将对现有的CompletableFuture<Integer>
进一步操作。- 与
thenRunAsync()
不同,cfi(4)
,cfi(5)
和cfi(6)
“then” 方法的参数是未包装的Integer
。 - 通过使用
voidr()
方法可以看到: AcceptAsync()
接收了一个Consumer
,因此不会产生结果。thenApplyAsync()
接收一个Function
, 并生成一个结果(该结果的类型可以不同于其输入类型) 。thenComposeAsync()
与thenApplyAsync()
非常相似,唯一区别在于其Function
必须产生已经包装在CompletableFuture
中的结果。
- 与
cfi(7)
示例演示了obtrudeValue()
,它强制将值作为结果。cfi(8)
使用toCompletableFuture()
从CompletionStage
生成一个CompletableFuture
。c.complete(9)
显示了如何通过给它一个结果来完成一个task
(future
) (与obtrudeValue()
相对,后者可能会迫使其结果替换该结果) 。- 如果你调用
CompletableFuture
中的cancel()
方法,如果已经完成此任务,则正常结束。 如果尚未完成,则使用CancellationException
完成此CompletableFuture
。 - 如果任务(
future
)完成,则getNow() 方法返回 CompletableFuture
的完成值,否则返回getNow()
的替换参数。 - 最后,我们看一下依赖
( dependents
) 的概念。如果我们将两个thenApplyAsync()
调用链路到CompletableFuture
上,则依赖项的数量不会增加,保持为1 。但是,如果我们另外将另一个thenApplyAsync()
直接附加到c
,则现在有两个依赖项:两个一起的链路和另一个单独附加的链路。- 这表明你可以使用一个
CompletionStage
,当它完成时,可以根据其结果派生多个新任务。
- 这表明你可以使用一个
结合CompletableFuture
第二种类型的 CompletableFuture
方法采用两种 CompletableFuture
并以各异方式将它们组合在一起。就像两个人在比赛一样CompletableFuture
通常比另一个更早地到达终点。这些方法允许你以不同的方式处理结果。
为了测试这一点,我们将创建一个任务,它有一个我们可以控制的定义了完成任务所需要的时间量的参数。
// concurrent/Workable.java
import java.util.concurrent.*;
import onjava.Nap;
public class Workable {
String id;
final double duration;
public Workable(String id, double duration) {
this.id = id;
this.duration = duration;
}
@Override
public String toString() {
return "Workable[" + id + "]";
}
public static Workable work(Workable tt) {
new Nap(tt.duration); // Seconds
tt.id = tt.id + "W";
System.out.println(tt);
return tt;
}
public static CompletableFuture<Workable> make(String id, double duration) {
return CompletableFuture
.completedFuture(
new Workable(id, duration)
)
.thenApplyAsync(Workable::work);
}
}
在 make()
中,work()
方法应用于CompletableFuture
。work()
需要一定的时间才能完成,然后它将字母
现在我们可以创建多个竞争的 CompletableFuture
,并使用 CompletableFuture
库中的各种方法来进行操作
// concurrent/DualCompletableOperations.java
import java.util.concurrent.*;
import static onjava.CompletableUtilities.*;
public class DualCompletableOperations {
static CompletableFuture<Workable> cfA, cfB;
static void init() {
cfA = Workable.make("A", 0.15);
cfB = Workable.make("B", 0.10); // Always wins
}
static void join() {
cfA.join();
cfB.join();
System.out.println("*****************");
}
public static void main(String[] args) {
init();
voidr(cfA.runAfterEitherAsync(cfB, () ->
System.out.println("runAfterEither")));
join();
init();
voidr(cfA.runAfterBothAsync(cfB, () ->
System.out.println("runAfterBoth")));
join();
init();
showr(cfA.applyToEitherAsync(cfB, w -> {
System.out.println("applyToEither: " + w);
return w;
}));
join();
init();
voidr(cfA.acceptEitherAsync(cfB, w -> {
System.out.println("acceptEither: " + w);
}));
join();
init();
voidr(cfA.thenAcceptBothAsync(cfB, (w1, w2) -> {
System.out.println("thenAcceptBoth: "
+ w1 + ", " + w2);
}));
join();
init();
showr(cfA.thenCombineAsync(cfB, (w1, w2) -> {
System.out.println("thenCombine: "
+ w1 + ", " + w2);
return w1;
}));
join();
init();
CompletableFuture<Workable>
cfC = Workable.make("C", 0.08),
cfD = Workable.make("D", 0.09);
CompletableFuture.anyOf(cfA, cfB, cfC, cfD)
.thenRunAsync(() ->
System.out.println("anyOf"));
join();
init();
cfC = Workable.make("C", 0.08);
cfD = Workable.make("D", 0.09);
CompletableFuture.allOf(cfA, cfB, cfC, cfD)
.thenRunAsync(() ->
System.out.println("allOf"));
join();
}
}
输出结果:
Workable[BW]
runAfterEither
Workable[AW]
*****************
Workable[BW]
Workable[AW]
runAfterBoth
*****************
Workable[BW]
applyToEither: Workable[BW]
Workable[BW]
Workable[AW]
*****************
Workable[BW]
acceptEither: Workable[BW]
Workable[AW]
*****************
Workable[BW]
Workable[AW]
thenAcceptBoth: Workable[AW], Workable[BW]
****************
Workable[BW]
Workable[AW]
thenCombine: Workable[AW], Workable[BW]
Workable[AW]
*****************
Workable[CW]
anyOf
Workable[DW]
Workable[BW]
Workable[AW]
*****************
Workable[CW]
Workable[DW]
Workable[BW]
Workable[AW]
*****************
allOf
- 为了方便访问, 将
cfA
和cfB
定义为static
的。init()
方法用于A
, B
初始化这两个变量,因为 B
总是给出比A
较短的延迟,所以总是win
的一方。join()
是在两个方法上调用join()
并显示边框的另一个便利方法。
- 所有这些 “
dual
” 方法都以一个CompletableFuture
作为调用该方法的对象,第二个CompletableFuture
作为第一个参数,然后是要执行的操作。 - 通过使用
showr()
和voidr()
可以看到, “run
”和“accept
”是终端操作,而“apply
”和“combine
”则生成新的payload-bearing
( 承载负载) 的CompletableFuture
。 - 方法的名称不言自明,你可以通过查看输出来验证这一点。一个特别有趣的方法是
combineAsync()
,它等待两个CompletableFuture
完成,然后将它们都交给一个BiFunction
,这个BiFunction
可以将结果加入到最终的CompletableFuture
的有效负载中。
模拟
作为使用 CompletableFuture
将一系列操作组合的示例,让我们模拟一下制作蛋糕的过程。在第一阶段,我们准备并将原料混合成面糊
// concurrent/Batter.java
import java.util.concurrent.*;
import onjava.Nap;
public class Batter {
static class Eggs {
}
static class Milk {
}
static class Sugar {
}
static class Flour {
}
static <T> T prepare(T ingredient) {
new Nap(0.1);
return ingredient;
}
static <T> CompletableFuture<T> prep(T ingredient) {
return CompletableFuture
.completedFuture(ingredient)
.thenApplyAsync(Batter::prepare);
}
public static CompletableFuture<Batter> mix() {
CompletableFuture<Eggs> eggs = prep(new Eggs());
CompletableFuture<Milk> milk = prep(new Milk());
CompletableFuture<Sugar> sugar = prep(new Sugar());
CompletableFuture<Flour> flour = prep(new Flour());
CompletableFuture
.allOf(eggs, milk, sugar, flour)
.join();
new Nap(0.1); // Mixing time
return CompletableFuture.completedFuture(new Batter());
}
}
每种原料都需要一些时间来准备。allOf()
等待所有的配料都准备好,然后使用更多些的时间将其混合成面糊。接下来,我们把单批面糊放入四个平底锅中烘烤。产品作为 CompletableFutures
流返回:
// concurrent/Baked.java
import java.util.concurrent.*;
import java.util.stream.*;
import onjava.Nap;
public class Baked {
static class Pan {
}
static Pan pan(Batter b) {
new Nap(0.1);
return new Pan();
}
static Baked heat(Pan p) {
new Nap(0.1);
return new Baked();
}
static CompletableFuture<Baked> bake(CompletableFuture<Batter> cfb) {
return cfb
.thenApplyAsync(Baked::pan)
.thenApplyAsync(Baked::heat);
}
public static Stream<CompletableFuture<Baked>> batch() {
CompletableFuture<Batter> batter = Batter.mix();
return Stream.of(
bake(batter),
bake(batter),
bake(batter),
bake(batter)
);
}
}
最后,我们制作了一批糖,并用它对蛋糕进行糖化:
// concurrent/FrostedCake.java
import java.util.concurrent.*;
import java.util.stream.*;
import onjava.Nap;
final class Frosting {
private Frosting() {
}
static CompletableFuture<Frosting> make() {
new Nap(0.1);
return CompletableFuture
.completedFuture(new Frosting());
}
}
public class FrostedCake {
public FrostedCake(Baked baked, Frosting frosting) {
new Nap(0.1);
}
@Override
public String toString() {
return "FrostedCake";
}
public static void main(String[] args) {
Baked.batch().forEach(
baked -> baked
.thenCombineAsync(Frosting.make(),
(cake, frosting) ->
new FrostedCake(cake, frosting))
.thenAcceptAsync(System.out::println)
.join());
}
}
一旦你习惯了这种背后的想法CompletableFuture
异常
与 CompletableFuture
在处理链中包装对象的方式相同,它也会缓冲异常。这些在处理时调用者是无感的,但仅当你尝试提取结果时才会被告知。为了说明它们是如何工作的,我们首先创建一个类,它在特定的条件下抛出一个异常
// concurrent/Breakable.java
import java.util.concurrent.*;
public class Breakable {
String id;
private int failcount;
public Breakable(String id, int failcount) {
this.id = id;
this.failcount = failcount;
}
@Override
public String toString() {
return "Breakable_" + id + " [" + failcount + "]";
}
public static Breakable work(Breakable b) {
if (--b.failcount == 0) {
System.out.println(
"Throwing Exception for " + b.id + ""
);
throw new RuntimeException(
"Breakable_" + b.id + " failed"
);
}
System.out.println(b);
return b;
}
}
当failcount
work()
方法时, failcount - 1
。当failcount - 1 = 0
时,work()
将抛出一个异常。如果传给 work()
的 failcount = 0
,work()
永远不会抛出异常。
注意,异常信息此示例中被抛出( RuntimeException
)
在下面示例 test()
方法中,work()
多次应用于 Breakable
,因此如果 failcount
在范围内,就会抛出异常。然而,在测试A
到E
中,你可以从输出中看到抛出了异常,但它们从未出现
// concurrent/CompletableExceptions.java
import java.util.concurrent.*;
public class CompletableExceptions {
static CompletableFuture<Breakable> test(String id, int failcount) {
return CompletableFuture.completedFuture(
new Breakable(id, failcount))
.thenApply(Breakable::work)
.thenApply(Breakable::work)
.thenApply(Breakable::work)
.thenApply(Breakable::work);
}
public static void main(String[] args) {
// Exceptions don't appear ...
test("A", 1);
test("B", 2);
test("C", 3);
test("D", 4);
test("E", 5);
// ... until you try to fetch the value:
try {
test("F", 2).get(); // or join()
} catch (Exception e) {
System.out.println(e.getMessage());
}
// Test for exceptions:
System.out.println(
test("G", 2).isCompletedExceptionally()
);
// Counts as "done":
System.out.println(test("H", 2).isDone());
// Force an exception:
CompletableFuture<Integer> cfi =
new CompletableFuture<>();
System.out.println("done? " + cfi.isDone());
cfi.completeExceptionally(
new RuntimeException("forced"));
try {
cfi.get();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
输出结果:
Throwing Exception for A
Breakable_B [1]
Throwing Exception for B
Breakable_C [2]
Breakable_C [1]
Throwing Exception for C
Breakable_D [3]
Breakable_D [2]
Breakable_D [1]
Throwing Exception for D
Breakable_E [4]
Breakable_E [3]
Breakable_E [2]
Breakable_E [1]
Breakable_F [1]
Throwing Exception for F
java.lang.RuntimeException: Breakable_F failed
Breakable_G [1]
Throwing Exception for G
true
Breakable_H [1]
Throwing Exception for H
true
done? false
java.lang.RuntimeException: forced
测试 A
到 E
运行到抛出异常,然后…并没有将抛出的异常暴露给调用方。只有在测试get()
时,我们才会看到抛出的异常。
测试 G
表明,你可以首先检查在处理期间是否抛出异常,而不抛出该异常。然而,H
CompletableFuture
中,而不管是否存在任何失败。
在连接或获取结果时,我们使用 CompletableFuture
提供的更复杂的机制来自动响应异常,而不是使用粗糙的 try-catch
。
你可以使用与我们看到的所有 CompletableFuture
相同的表单来完成此操作CompletableFuture
调用。有三个选项 exceptionally()
,handle()
, whenComplete()
:
// concurrent/CatchCompletableExceptions.java
import java.util.concurrent.*;
public class CatchCompletableExceptions {
static void handleException(int failcount) {
// Call the Function only if there's an
// exception, must produce same type as came in:
CompletableExceptions
.test("exceptionally", failcount)
.exceptionally((ex) -> { // Function
if (ex == null)
System.out.println("I don't get it yet");
return new Breakable(ex.getMessage(), 0);
})
.thenAccept(str ->
System.out.println("result: " + str));
// Create a new result (recover):
CompletableExceptions
.test("handle", failcount)
.handle((result, fail) -> { // BiFunction
if (fail != null)
return "Failure recovery object";
else
return result + " is good";
})
.thenAccept(str ->
System.out.println("result: " + str));
// Do something but pass the same result through:
CompletableExceptions
.test("whenComplete", failcount)
.whenComplete((result, fail) -> { // BiConsumer
if (fail != null)
System.out.println("It failed");
else
System.out.println(result + " OK");
})
.thenAccept(r ->
System.out.println("result: " + r));
}
public static void main(String[] args) {
System.out.println("**** Failure Mode ****");
handleException(2);
System.out.println("**** Success Mode ****");
handleException(0);
}
}
输出结果:
**** Failure Mode ****
Breakable_exceptionally [1]
Throwing Exception for exceptionally
result: Breakable_java.lang.RuntimeException:
Breakable_exceptionally failed [0]
Breakable_handle [1]
Throwing Exception for handle
result: Failure recovery object
Breakable_whenComplete [1]
Throwing Exception for whenComplete
It failed
**** Success Mode ****
Breakable_exceptionally [-1]
Breakable_exceptionally [-2]
Breakable_exceptionally [-3]
Breakable_exceptionally [-4]
result: Breakable_exceptionally [-4]
Breakable_handle [-1]
Breakable_handle [-2]
Breakable_handle [-3]
Breakable_handle [-4]
result: Breakable_handle [-4] is good
Breakable_whenComplete [-1]
Breakable_whenComplete [-2]
Breakable_whenComplete [-3]
Breakable_whenComplete [-4]
Breakable_whenComplete [-4] OK
result: Breakable_whenComplete [-4]
-
exceptionally()
参数仅在出现异常时才运行。exceptionally()
局限性在于,该函数只能返回输入类型相同的值。 -
exceptionally()
通过将一个好的对象插入到流中来恢复到一个可行的状态。 -
handle()
一致被调用来查看是否发生异常(必须检查fail 是否为true ) 。-
但是
handle()
可以生成任何新类型,所以它允许执行处理,而不是像使用exceptionally()
那样简单地恢复。 -
whenComplete()
类似于handle() ,同样必须测试它是否失败,但是参数是一个消费者,并且不修改传递给它的结果对象。
-
流异常(Stream Exception)
通过修改
// concurrent/StreamExceptions.java
import java.util.concurrent.*;
import java.util.stream.*;
public class StreamExceptions {
static Stream<Breakable> test(String id, int failcount) {
return Stream.of(new Breakable(id, failcount))
.map(Breakable::work)
.map(Breakable::work)
.map(Breakable::work)
.map(Breakable::work);
}
public static void main(String[] args) {
// No operations are even applied ...
test("A", 1);
test("B", 2);
Stream<Breakable> c = test("C", 3);
test("D", 4);
test("E", 5);
// ... until there's a terminal operation:
System.out.println("Entering try");
try {
c.forEach(System.out::println); // [1]
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
输出结果:
Entering try
Breakable_C [2]
Breakable_C [1]
Throwing Exception for C
Breakable_C failed
使用 CompletableFuture
,我们可以看到测试forEach()
CompletableFuture
执行工作并捕获任何异常供以后检索。比较这两者并不容易,因为 Stream
在没有终端操作的情况下根本不做任何事情——但是流绝对不会存储它的异常。
检查性异常
CompletableFuture
和 parallel Stream
都不支持包含检查性异常的操作。相反,你必须在调用操作时处理检查到的异常,这会产生不太优雅的代码:
// concurrent/ThrowsChecked.java
import java.util.stream.*;
import java.util.concurrent.*;
public class ThrowsChecked {
class Checked extends Exception {}
static ThrowsChecked nochecked(ThrowsChecked tc) {
return tc;
}
static ThrowsChecked withchecked(ThrowsChecked tc) throws Checked {
return tc;
}
static void testStream() {
Stream.of(new ThrowsChecked())
.map(ThrowsChecked::nochecked)
// .map(ThrowsChecked::withchecked); // [1]
.map(
tc -> {
try {
return withchecked(tc);
} catch (Checked e) {
throw new RuntimeException(e);
}
});
}
static void testCompletableFuture() {
CompletableFuture
.completedFuture(new ThrowsChecked())
.thenApply(ThrowsChecked::nochecked)
// .thenApply(ThrowsChecked::withchecked); // [2]
.thenApply(
tc -> {
try {
return withchecked(tc);
} catch (Checked e) {
throw new RuntimeException(e);
}
});
}
}
如果你试图像使用 nochecked()
那样使用 withchecked()
的方法引用,编译器会在 [1]
和 [2]
中报错。相反,你必须写出
死锁
由于任务可以被阻塞,因此一个任务有可能卡在等待另一个任务上,而后者又在等待别的任务,这样一直下去,知道这个链条上的任务又在等待第一个任务释放锁。这得到了一个任务之间相互等待的连续循环, 没有哪个线程能继续, 这称之为死锁6
如果你运行一个程序,而它马上就死锁了, 你可以立即跟踪下去。真正的问题在于,程序看起来工作良好, 但是具有潜在的死锁危险。这时, 死锁可能发生,而事先却没有任何征兆, 所以 bug
会潜伏在你的程序例,直到客户发现它出乎意料的发生(以一种几乎肯定是很难重现的方式发生Essger Dijkstra
)发明的“哲学家进餐
// concurrent/StickHolder.java
import java.util.concurrent.*;
public class StickHolder {
private static class Chopstick {
}
private Chopstick stick = new Chopstick();
private BlockingQueue<Chopstick> holder =
new ArrayBlockingQueue<>(1);
public StickHolder() {
putDown();
}
public void pickUp() {
try {
holder.take(); // Blocks if unavailable
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public void putDown() {
try {
holder.put(stick);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
为简单起见,Chopstick
static
StickHolder
生产的,而是在其类中保持私有的。
如果您调用了pickUp()
,而 stick
不可用,那么pickUp()
将阻塞该 stick
,直到另一个哲学家调用putDown()
将 stick
返回。
注意,该类中的所有线程安全都是通过 BlockingQueue
实现的。
每个哲学家都是一项任务,他们试图把筷子分别 pickUp()
在左手和右手上,这样筷子才能吃东西,然后通过 putDown()
放下 stick
。
// concurrent/Philosopher.java
public class Philosopher implements Runnable {
private final int seat;
private final StickHolder left, right;
public Philosopher(int seat, StickHolder left, StickHolder right) {
this.seat = seat;
this.left = left;
this.right = right;
}
@Override
public String toString() {
return "P" + seat;
}
@Override
public void run() {
while (true) {
// System.out.println("Thinking"); // [1]
right.pickUp();
left.pickUp();
System.out.println(this + " eating");
right.putDown();
left.putDown();
}
}
}
没有两个哲学家可以同时成功调用
结果是一个看似无辜的程序陷入了死锁。我在这里使用数组而不是集合,只是因为这种语法更简洁:
// concurrent/DiningPhilosophers.java
// Hidden deadlock
// {ExcludeFromGradle} Gradle has trouble
import java.util.*;
import java.util.concurrent.*;
import onjava.Nap;
public class DiningPhilosophers {
private StickHolder[] sticks;
private Philosopher[] philosophers;
public DiningPhilosophers(int n) {
sticks = new StickHolder[n];
Arrays.setAll(sticks, i -> new StickHolder());
philosophers = new Philosopher[n];
Arrays.setAll(philosophers, i ->
new Philosopher(i,
sticks[i], sticks[(i + 1) % n])); // [1]
// Fix by reversing stick order for this one:
// philosophers[1] = // [2]
// new Philosopher(0, sticks[0], sticks[1]);
Arrays.stream(philosophers)
.forEach(CompletableFuture::runAsync); // [3]
}
public static void main(String[] args) {
// Returns right away:
new DiningPhilosophers(5); // [4]
// Keeps main() from exiting:
new Nap(3, "Shutdown");
}
}
- 当你停止查看输出时,该程序将死锁。但是,根据你的计算机配置,你可能不会看到死锁。看来这取决于计算机上的内核数7。两个核心不会产生死锁,但两核以上却很容易产生死锁。
- 此行为使该示例更好地说明了死锁,因为你可能正在具有
2 核的计算机上编写程序(如果确实是导致问题的原因) ,并且确信该程序可以正常工作,只能启动它将其安装在另一台计算机上时出现死锁。请注意,不能因为你没或不容易看到死锁,这并不意味着此程序不会在2 核机器上发生死锁。 该程序仍然有死锁倾向,只是很少发生——可以说是最糟糕的情况,因为问题不容易出现。 - 在
DiningPhilosophers
的构造方法中,每个哲学家都获得一个左右筷子的引用。除最后一个哲学家外,都是通过把哲学家放在下一双空闲筷子之间来初始化:- 最后一位哲学家得到了第
0 根筷子作为他的右筷子,所以圆桌就完成。 - 那是因为最后一位哲学家正坐在第一个哲学家的旁边,而且他们俩都共用零筷子。
[1] 显示了以n 为模数选择的右筷子,将最后一个哲学家绕到第一个哲学家的旁边。
- 最后一位哲学家得到了第
- 现在,所有哲学家都可以尝试吃饭,每个哲学家都在旁边等待哲学家放下筷子。
- 为了让每个哲学家在
[3] 上运行,调用runAsync()
,这意味着DiningPhilosophers 的构造函数立即返回到[4] 。 - 如果没有任何东西阻止
main()
完成,程序就会退出,不会做太多事情。 Nap
对象阻止main()
退出,然后在三秒后强制退出( 假设/ 可能是) 死锁程序。- 在给定的配置中,哲学家几乎不花时间思考。因此,他们在吃东西的时候都争着用筷子,而且往往很快就会陷入僵局。你可以改变这个
:
- 为了让每个哲学家在
-
通过增加
[4] 的值来添加更多哲学家。 -
在
Philosopher.java 中取消注释行[1] 。
任一种方法都会减少死锁的可能性,这表明编写并发程序并认为它是安全的危险,因为它似乎“在我的机器上运行正常”。你可以轻松地说服自己该程序没有死锁,即使它不是。这个示例相当有趣,因为它演示了看起来可以正确运行,但实际上会可能发生死锁的程序。
要修正死锁问题,你必须明白,当以下四个条件同时满足时,就会发生死锁:
- 互斥条件。任务使用的资源中至少有一个不能共享的。 这里,一根筷子一次就只能被一个哲学家使用。
- 至少有一个任务它必须持有一个资源且正在等待获取一个被当前别的任务持有的资源。也就是说,要发生死锁,哲学家必须拿着一根筷子并且等待另一根。
- 资源不能被任务抢占, 任务必须把资源释放当作普通事件。哲学家很有礼貌,他们不会从其它哲学家那里抢筷子。
- 必须有循环等待, 这时,一个任务等待其它任务所持有的资源, 后者又在等待另一个任务所持有的资源, 这样一直下去,知道有一个任务在等待第一个任务所持有的资源, 使得大家都被锁住。 在
DiningPhilosophers.java
中, 因为每个哲学家都试图先得到右边的 筷子, 然后得到左边的 筷子, 所以发生了循环等待。
因为必须满足所有条件才能导致死锁,所以要阻止死锁的话,只需要破坏其中一个即可。在此程序中,防止死锁的一种简单方法是打破第四个条件。之所以会发生这种情况,是因为每个哲学家都尝试按照特定的顺序拾起自己的筷子:先右后左。因此,每个哲学家都有可能在等待左手的同时握住右手的筷子,从而导致循环等待状态。但是,如果其中一位哲学家尝试首先拿起左筷子,则该哲学家决不会阻止紧邻右方的哲学家拿起筷子,从而排除了循环等待。
在
构造方法非线程安全
当你在脑子里想象一个对象构造的过程,你会很容易认为这个过程是线程安全的。毕竟,在对象初始化完成前对外不可见,所以又怎会对此产生争议呢?确实,
不幸的是,对象的构造过程如其他操作一样,也会受到共享内存并发问题的影响,只是作用机制可能更微妙罢了。
设想下使用一个
//concurrent/HasID.java
public interface HasID {
int getID();
}
然后
// concurrent/StaticIDField.java
public class StaticIDField implements HasID {
private static int counter = 0;
private int id = counter++;
public int getID() { return id; }
}
如你所想,该类是个简单无害的类,它甚至都没一个显式的构造器来引发问题。当我们运行多个用于创建此类对象的线程时,究竟会发生什么?为了搞清楚这点,我们做了以下测试。代码示例:
// concurrent/IDChecker.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import java.util.concurrent.*;
import com.google.common.collect.Sets;
public class IDChecker {
public static final int SIZE = 100_000;
static class MakeObjects implements
Supplier<List<Integer>> {
private Supplier<HasID> gen;
MakeObjects(Supplier<HasID> gen) {
this.gen = gen;
}
@Override public List<Integer> get() {
return Stream.generate(gen)
.limit(SIZE)
.map(HasID::getID)
.collect(Collectors.toList());
}
}
public static void test(Supplier<HasID> gen) {
CompletableFuture<List<Integer>>
groupA = CompletableFuture.supplyAsync(new
MakeObjects(gen)),
groupB = CompletableFuture.supplyAsync(new
MakeObjects(gen));
groupA.thenAcceptBoth(groupB, (a, b) -> {
System.out.println(
Sets.intersection(
Sets.newHashSet(a),
Sets.newHashSet(b)).size());
}).join();
}
}
get()
方法。通过从每个 HasID
对象提取 ID
并放入列表中来生成这个列表对象,而 test()
方法则创建了两个并行的
使用intersection()
ID
(使用谷歌retainAll()
方法速度快得多
现在我们可以测试上面的
// concurrent/TestStaticIDField.java
public class TestStaticIDField {
public static void main(String[] args) {
IDChecker.test(StaticIDField::new);
}
}
输出结果:
13287
结果中出现了很多重复项。很显然,纯静态 int
用于构造过程并不是线程安全的。让我们使用
// concurrent/GuardedIDField.java
import java.util.concurrent.atomic.*;
public class GuardedIDField implements HasID {
private static AtomicInteger counter = new
AtomicInteger();
private int id = counter.getAndIncrement();
public int getID() { return id; }
public static void main(String[] args) { IDChecker.test(GuardedIDField::new);
}
}
输出结果:
0
构造器有一种更微妙的状态共享方式:通过构造器参数:
// concurrent/SharedConstructorArgument.java
import java.util.concurrent.atomic.*;
interface SharedArg{
int get();
}
class Unsafe implements SharedArg{
private int i = 0;
public int get(){
return i++;
}
}
class Safe implements SharedArg{
private static AtomicInteger counter = new AtomicInteger();
public int get(){
return counter.getAndIncrement();
}
}
class SharedUser implements HasID{
private final int id;
SharedUser(SharedArg sa){
id = sa.get();
}
@Override
public int getID(){
return id;
}
}
public class SharedConstructorArgument{
public static void main(String[] args){
Unsafe unsafe = new Unsafe();
IDChecker.test(() -> new SharedUser(unsafe));
Safe safe = new Safe();
IDChecker.test(() -> new SharedUser(safe));
}
}
输出结果:
24838
0
在这里,
同步构造器并不被synchronized
// concurrent/SynchronizedConstructor.java
import java.util.concurrent.atomic.*;
class SyncConstructor implements HasID{
private final int id;
private static Object constructorLock =
new Object();
SyncConstructor(SharedArg sa){
synchronized (constructorLock){
id = sa.get();
}
}
@Override
public int getID(){
return id;
}
}
public class SynchronizedConstructor{
public static void main(String[] args){
Unsafe unsafe = new Unsafe();
IDChecker.test(() -> new SyncConstructor(unsafe));
}
}
输出结果:
0
// concurrent/SynchronizedFactory.java
import java.util.concurrent.atomic.*;
final class SyncFactory implements HasID{
private final int id;
private SyncFactory(SharedArg sa){
id = sa.get();
}
@Override
public int getID(){
return id;
}
public static synchronized SyncFactory factory(SharedArg sa){
return new SyncFactory(sa);
}
}
public class SynchronizedFactory{
public static void main(String[] args){
Unsafe unsafe = new Unsafe();
IDChecker.test(() -> SyncFactory.factory(unsafe));
}
}
输出结果:
0
通过同步静态工厂方法,可以在构造过程中锁定
这些示例充分表明了在并发
复杂性和代价
假设你正在做披萨,我们把从整个流程的当前步骤到下一个步骤所需的工作量,在这里一一表示为枚举变量的一部分:
// concurrent/Pizza.java import java.util.function.*;
import onjava.Nap;
public class Pizza{
public enum Step{
DOUGH(4), ROLLED(1), SAUCED(1), CHEESED(2),
TOPPED(5), BAKED(2), SLICED(1), BOXED(0);
int effort;// Needed to get to the next step
Step(int effort){
this.effort = effort;
}
Step forward(){
if (equals(BOXED)) return BOXED;
new Nap(effort * 0.1);
return values()[ordinal() + 1];
}
}
private Step step = Step.DOUGH;
private final int id;
public Pizza(int id){
this.id = id;
}
public Pizza next(){
step = step.forward();
System.out.println("Pizza " + id + ": " + step);
return this;
}
public Pizza next(Step previousStep){
if (!step.equals(previousStep))
throw new IllegalStateException("Expected " +
previousStep + " but found " + step);
return next();
}
public Pizza roll(){
return next(Step.DOUGH);
}
public Pizza sauce(){
return next(Step.ROLLED);
}
public Pizza cheese(){
return next(Step.SAUCED);
}
public Pizza toppings(){
return next(Step.CHEESED);
}
public Pizza bake(){
return next(Step.TOPPED);
}
public Pizza slice(){
return next(Step.BAKED);
}
public Pizza box(){
return next(Step.SLICED);
}
public boolean complete(){
return step.equals(Step.BOXED);
}
@Override
public String toString(){
return "Pizza" + id + ": " + (step.equals(Step.BOXED) ? "complete" : step);
}
}
这只算得上是一个平凡的状态机,就像
制作一个披萨,当披萨饼最终被放在盒子中时,就算完成最终任务了。 如果一个人在做一个披萨饼,那么所有步骤都是线性进行的,即一个接一个地进行:
// concurrent/OnePizza.java
import onjava.Timer;
public class OnePizza{
public static void main(String[] args){
Pizza za = new Pizza(0);
System.out.println(Timer.duration(() -> {
while (!za.complete()) za.next();
}));
}
}
输出结果:
Pizza 0: ROLLED
Pizza 0: SAUCED
Pizza 0: CHEESED
Pizza 0: TOPPED
Pizza 0: BAKED
Pizza 0: SLICED
Pizza 0: BOXED
1622
时间以毫秒为单位,加总所有步骤的工作量,会得出与我们的期望值相符的数字。 如果你以这种方式制作了五个披萨,那么你会认为它花费的时间是原来的五倍。 但是,如果这还不够快怎么办? 我们可以从尝试并行流方法开始:
// concurrent/PizzaStreams.java
// import java.util.*; import java.util.stream.*;
import onjava.Timer;
public class PizzaStreams{
static final int QUANTITY = 5;
public static void main(String[] args){
Timer timer = new Timer();
IntStream.range(0, QUANTITY)
.mapToObj(Pizza::new)
.parallel()//[1]
.forEach(za -> { while(!za.complete()) za.next(); }); System.out.println(timer.duration());
}
}
输出结果:
Pizza 2: ROLLED
Pizza 0: ROLLED
Pizza 1: ROLLED
Pizza 4: ROLLED
Pizza 3:ROLLED
Pizza 2:SAUCED
Pizza 1:SAUCED
Pizza 0:SAUCED
Pizza 4:SAUCED
Pizza 3:SAUCED
Pizza 2:CHEESED
Pizza 1:CHEESED
Pizza 0:CHEESED
Pizza 4:CHEESED
Pizza 3:CHEESED
Pizza 2:TOPPED
Pizza 1:TOPPED
Pizza 0:TOPPED
Pizza 4:TOPPED
Pizza 3:TOPPED
Pizza 2:BAKED
Pizza 1:BAKED
Pizza 0:BAKED
Pizza 4:BAKED
Pizza 3:BAKED
Pizza 2:SLICED
Pizza 1:SLICED
Pizza 0:SLICED
Pizza 4:SLICED
Pizza 3:SLICED
Pizza 2:BOXED
Pizza 1:BOXED
Pizza 0:BOXED
Pizza 4:BOXED
Pizza 3:BOXED
1739
现在,我们制作五个披萨的时间与制作单个披萨的时间就差不多了。 尝试删除标记为
forEach()
内完成所有工作,如果我们将其各个步骤用映射的方式一步一步处理,情况会有所不同吗?
// concurrent/PizzaParallelSteps.java
import java.util.*;
import java.util.stream.*;
import onjava.Timer;
public class PizzaParallelSteps{
static final int QUANTITY = 5;
public static void main(String[] args){
Timer timer = new Timer();
IntStream.range(0, QUANTITY)
.mapToObj(Pizza::new)
.parallel()
.map(Pizza::roll)
.map(Pizza::sauce)
.map(Pizza::cheese)
.map(Pizza::toppings)
.map(Pizza::bake)
.map(Pizza::slice)
.map(Pizza::box)
.forEach(za -> System.out.println(za));
System.out.println(timer.duration());
}
}
输出结果:
Pizza 2: ROLLED
Pizza 0: ROLLED
Pizza 1: ROLLED
Pizza 4: ROLLED
Pizza 3: ROLLED
Pizza 1: SAUCED
Pizza 0: SAUCED
Pizza 2: SAUCED
Pizza 3: SAUCED
Pizza 4: SAUCED
Pizza 1: CHEESED
Pizza 0: CHEESED
Pizza 2: CHEESED
Pizza 3: CHEESED
Pizza 4: CHEESED
Pizza 0: TOPPED
Pizza 2: TOPPED
Pizza 1: TOPPED
Pizza 3: TOPPED
Pizza 4: TOPPED
Pizza 1: BAKED
Pizza 2: BAKED
Pizza 0: BAKED
Pizza 4: BAKED
Pizza 3: BAKED
Pizza 0: SLICED
Pizza 2: SLICED
Pizza 1: SLICED
Pizza 3: SLICED
Pizza 4: SLICED
Pizza 1: BOXED
Pizza1: complete
Pizza 2: BOXED
Pizza 0: BOXED
Pizza2: complete
Pizza0: complete
Pizza 3: BOXED
Pizza 4: BOXED
Pizza4: complete
Pizza3: complete
1738
答案是“否”,事后看来这并不奇怪,因为每个披萨都需要按顺序执行步骤。因此,没法通过分步执行操作来进一步提高速度,就像上文的 PizzaParallelSteps.java
里面展示的一样。
我们可以使用
// concurrent/CompletablePizza.java
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
import onjava.Timer;
public class CompletablePizza{
static final int QUANTITY = 5;
public static CompletableFuture<Pizza> makeCF(Pizza za){
return CompletableFuture
.completedFuture(za)
.thenApplyAsync(Pizza::roll)
.thenApplyAsync(Pizza::sauce)
.thenApplyAsync(Pizza::cheese)
.thenApplyAsync(Pizza::toppings)
.thenApplyAsync(Pizza::bake)
.thenApplyAsync(Pizza::slice)
.thenApplyAsync(Pizza::box);
}
public static void show(CompletableFuture<Pizza> cf){
try{
System.out.println(cf.get());
} catch (Exception e){
throw new RuntimeException(e);
}
}
public static void main(String[] args){
Timer timer = new Timer();
List<CompletableFuture<Pizza>> pizzas =
IntStream.range(0, QUANTITY)
.mapToObj(Pizza::new)
.map(CompletablePizza::makeCF)
.collect(Collectors.toList());
System.out.println(timer.duration());
pizzas.forEach(CompletablePizza::show);
System.out.println(timer.duration());
}
}
输出结果:
169
Pizza 0: ROLLED
Pizza 1: ROLLED
Pizza 2: ROLLED
Pizza 4: ROLLED
Pizza 3: ROLLED
Pizza 1: SAUCED
Pizza 0: SAUCED
Pizza 2: SAUCED
Pizza 4: SAUCED
Pizza 3: SAUCED
Pizza 0: CHEESED
Pizza 4: CHEESED
Pizza 1: CHEESED
Pizza 2: CHEESED
Pizza 3: CHEESED
Pizza 0: TOPPED
Pizza 4: TOPPED
Pizza 1: TOPPED
Pizza 2: TOPPED
Pizza 3: TOPPED
Pizza 0: BAKED
Pizza 4: BAKED
Pizza 1: BAKED
Pizza 3: BAKED
Pizza 2: BAKED
Pizza 0: SLICED
Pizza 4: SLICED
Pizza 1: SLICED
Pizza 3: SLICED
Pizza 2: SLICED
Pizza 4: BOXED
Pizza 0: BOXED
Pizza0: complete
Pizza 1: BOXED
Pizza1: complete
Pizza 3: BOXED
Pizza 2: BOXED
Pizza2: complete
Pizza3: complete
Pizza4: complete
1797
并行流和
而当工作的各个部分内容各不相同时,使用
对于披萨问题,结果似乎也没有什么不同。实际上,并行流方法看起来更简洁,仅出于这个原因,我认为并行流作为解决问题的首次尝试方法更具吸引力。
由于制作披萨总需要一定的时间,无论你使用哪种并发方法,你能做到的最好情况,是在制作一个披萨的相同时间内制作
使用
本章小结
需要并发的唯一理由是“等待太多”。这也可以包括用户界面的响应速度,但是由于
如果并发很容易,则没有理由拒绝并发。 正因为并发实际上很难,所以你应该仔细考虑是否值得为此付出努力,并考虑你能否以其他方式提升速度。
例如,迁移到更快的硬件(这可能比消耗程序员的时间要便宜得多)或者将程序分解成多个部分,然后在不同的机器上运行这些部分。
奥卡姆剃刀是一个经常被误解的原则。 我看过至少一部电影,他们将其定义为”最简单的解决方案是正确的解决方案“,就好像这是某种毋庸置疑的法律。实际上,这是一个准则:面对多种方法时,请先尝试需要最少假设的方法。 在编程世界中,这已演变为“尝试可能可行的最简单的方法”。当你了解了特定工具的知识时——就像你现在了解了有关并发性的知识一样,你可能会很想使用它,或者提前规定你的解决方案必须能够“速度飞快”,从而来证明从一开始就进行并发设计是合理的。但是,我们的奥卡姆剃刀编程版本表示你应该首先尝试最简单的方法(这种方法开发起来也更便宜
由于我出身于底层学术背景(物理学和计算机工程
缺点
并发编程的主要缺点是:
-
在线程等待共享资源时会降低速度。
-
线程管理产生额外
CPU 开销。 -
糟糕的设计决策带来无法弥补的复杂性。
-
诸如饥饿,竞速,死锁和活锁(多线程各自处理单个任务而整体却无法完成)之类的问题。
-
跨平台的不一致。 通过一些示例,我发现了某些计算机上很快出现的竞争状况,而在其他计算机上却没有。 如果你在后者上开发程序,则在分发程序时可能会感到非常惊讶。
另外,并发的应用是一门艺术。
共享内存陷阱
并发性的主要困难之一是因为可能有多个任务共享一个资源(例如对象中的内存
我花了多年的时间研究并发。 我了解到你永远无法相信使用共享内存并发的程序可以正常工作。 你可以轻易发现它是错误的,但永远无法证明它是正确的。 这是众所周知的并发原则之一。10
我遇到了许多人,他们对编写正确的线程程序的能力充满信心。 我偶尔开始认为我也可以做好。 对于一个特定的程序,我最初是在只有单个
再举一个例子,哲学家就餐的问题可以很容易地进行调整,因此几乎不会产生死锁,这会给你一种一切都棒极了的印象。当涉及到共享内存并发编程时,你永远不应该对自己的编程能力变得过于自信。
This Albatross is Big
如果你对
事实证明,在
在本书的其他地方,我谈到了
其他类库
本章重点介绍了相对安全易用的并行工具流和
考虑为并发设计的语言
通常,请谨慎地使用并发。 如果需要使用它,请尝试使用最现代的方法:并行流或
如果你的并发问题变得比高级
你很容易被一种语言绑定,迫使自己尝试使用该语言来做所有事情。 一个常见的示例是构建
心理上的便利是一个合理的考虑因素。 但是,我希望我在本章(以及附录:并发底层原理 )中已经表明
无论使用特定的语言、库使得并发看起来多么简单,都要将其视为一种妖术,因为总是有东西会在你最不期望出现的时候咬你。
拓展阅读
《Java Concurrency in Practice
-
例如
,Eric-Raymond 在“Unix 编程艺术”(Addison-Wesley,2004)中提出了一个很好的案例。 ↩︎ -
可以说,试图将并发性用于后续语言是一种注定要失败的方法,但你必须得出自己的结论
↩︎ -
有人谈论在
Java ——10 中围绕泛型做一些类似的基本改进,这将是非常令人难以置信的。 ↩︎ -
这是一种有趣的,虽然不一致的方法。通常,我们期望在公共接口上使用显式类表示不同的行为
↩︎ -
不,永远不会有纯粹的功能性
Java 。我们所能期望的最好的是一种在JVM 上运行的全新语言。 ↩︎ -
当两个任务能够更改其状态以使它们不会被阻止但它们从未取得任何有用的进展时,你也可以使用活动锁。 ↩︎
-
而不是超线程;通常每个内核有两个超线程,并且在询问内核数量时,本书所使用的
Java 版本会报告超线程的数量。超线程产生了更快的上下文切换,但是只有实际的内核才真的工作,而不是超线程。 ↩ ↩︎ -
库就在那里用于调用,而语言本身就被设计用于此目的,但实际上它很少发生,以至于可以说”没有“。↩ ↩︎
-
举例来说,如果没有
Flyweight 设计模式,在工程中创建数百万个对象用于有限元分析可能在Java 中不可行。↩ ↩︎ -
在科学中,虽然从来没有一种理论被证实过,但是一种理论必须是可证伪的才有意义。而对于并发性,我们大部分时间甚至都无法得到这种可证伪性。↩ ↩︎
-
尽管
Go 语言显示了 FFI 的前景,但在撰写本文时,它并未提供跨所有平台的解决方案。 ↩︎