diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md index a03a473..18a0756 100644 --- a/docs/book/24-Concurrent-Programming.md +++ b/docs/book/24-Concurrent-Programming.md @@ -245,9 +245,9 @@ C语言是面向过程语言,这限制了它的野心。这些限制使附加 不幸的是,为了在更高级别的语言中获得并发性,所有语言功能都会受到影响,包括最基本的功能,例如标识符代表可变值。在简化并发编程中,所有函数和方法中为了保持事物不变和防止副作用都要做出巨大的改变(这些是纯函数式编程语言的基础),但当时对于主流语言的创建者来说似乎是奇怪的想法。最初的Java设计师要么没有意识到这些选择,要么认为它们太不同了,并且会劝退许多潜在的语言使用者。我们可以慷慨地说,语言设计社区当时根本没有足够的经验来理解调整在线程库中的影响。 -Java实验告诉我们,结果是悄然灾难性的。程序员很容易陷入认为Java 线程并不那么困难的陷阱。似乎工作的程序充满了微妙的并发bug。 +Java实验告诉我们,结果是悄然灾难性的。程序员很容易陷入认为Java 线程并不那么困难的陷阱。表面上看起来正常工作的程序实际上充满了微妙的并发bug。 -为了获得正确的并发性,语言功能必须从头开始设计并考虑并发性。这艘船航行了;Java将不再是为并发而设计的语言,而只是一种允许它的语言。 +为了获得正确的并发性,语言功能必须从头开始设计并考虑并发性。木已成舟;Java 将不再是为并发而设计的语言,而只是一种允许并发的语言。 尽管有这些基本的不可修复的缺陷,但令人印象深刻的是它已经走了这么远。Java的后续版本添加了库,以便在使用并发时提升抽象级别。事实上,我根本不会想到有可能在Java 8中进行改进:并行流和**CompletableFutures** - 这是惊人的史诗般的变化,我会惊奇地重复的查看它[^3]。 @@ -291,7 +291,7 @@ Java 8 CompletableFuture是一个更好的解决方案:它允许你将操作 ## 并行流 -Java 8流的一个显着优点是,在某些情况下,它们可以很容易地并行化。这来自仔细的库设计,特别是流使用内部迭代的方式 - 也就是说,它们控制着自己的迭代器。特别是,他们使用一种特殊的迭代器,称为Spliterator,它被限制为易于自动分割。这产生了相当神奇的结果,即能够简单用parallel()然后流中的所有内容都作为一组并行任务运行。如果你的代码是使用Streams编写的,那么并行化以提高速度似乎是一种琐事 +Java 8流的一个显著优点是,在某些情况下,它们可以很容易地并行化。这来自仔细的库设计,特别是流使用内部迭代的方式 - 也就是说,它们控制着自己的迭代器。特别是,他们使用一种特殊的迭代器,称为Spliterator,它被限制为易于自动分割。我们只需要念 `.parallel()` 就会产生魔法般的结果,流中的所有内容都作为一组并行任务运行。如果你的代码是使用Streams编写的,那么并行化以提高速度似乎是一种琐事 例如,考虑来自Streams的Prime.java。查找质数可能是一个耗时的过程,我们可以看到该程序的计时: @@ -332,19 +332,19 @@ public class ParallelPrime { 1224 ``` -请注意,这不是微基准测试,因为我们计时整个程序。我们将数据保存在磁盘上以防止过激的优化;如果我们没有对结果做任何事情,那么一个高级的编译器可能会观察到程序没有意义并且消除了计算(这不太可能,但并非不可能)。请注意使用nio2库编写文件的简单性(在[文件](./17-Files.md)一章中有描述)。 +请注意,这不是微基准测试,因为我们计时整个程序。我们将数据保存在磁盘上以防止编译器过激的优化;如果我们没有对结果做任何事情,那么一个高级的编译器可能会观察到程序没有意义并且终止了计算(这不太可能,但并非不可能)。请注意使用nio2库编写文件的简单性(在[文件](./17-Files.md)一章中有描述)。 -当我注释掉[1] parallel()行时,我的结果大约是parallel()的三倍。 +当我注释掉[1] parallel()行时,我的结果用时大约是parallel()的三倍。 并行流似乎是一个甜蜜的交易。你所需要做的就是将编程问题转换为流,然后插入parallel()以加快速度。实际上,有时候这很容易。但遗憾的是,有许多陷阱。 - parallel()不是灵丹妙药 -作为对流和并行流的不确定性的探索,让我们看一个看似简单的问题:求和数字的增量序列。事实证明这是一个令人惊讶的数量,并且我将冒险将它们进行比较 - 试图小心,但承认我可能会在计时代码执行时遇到许多基本陷阱之一。结果可能有一些缺陷(例如JVM没有“升温”),但我认为它仍然提供了一些有用的指示。 +作为对流和并行流的不确定性的探索,让我们看一个看似简单的问题:对增长的数字序列进行求和。事实证明有大量的方式去实现它,并且我将冒险用计时器将它们进行比较 - 我会尽量小心,但我承认我可能会在计时代码执行时遇到许多基本陷阱之一。结果可能有一些缺陷(例如JVM没有“热身”),但我认为它仍然提供了一些有用的指示。 -我将从一个计时方法rigorously 开始,它采用**LongSupplier**,测量**getAsLong()**调用的长度,将结果与**checkValue**进行比较并显示结果。 +我将从一个计时方法**timeTest()**开始,它采用**LongSupplier**,测量**getAsLong()**调用的长度,将结果与**checkValue**进行比较并显示结果。 -请注意,一切都必须严格使用**long**;我花了一些时间发现隐蔽的溢出,然后才意识到在重要的地方错过了**long**。 +请注意,一切都必须严格使用**long**;我花了一些时间发现隐蔽的溢出,然后才意识到在重要的地方错过了**long**。 所有关于时间和内存的数字和讨论都是指“我的机器”。你的经历可能会有所不同。 @@ -394,19 +394,19 @@ Sum Stream Parallel: 46ms Sum Iterated: 284ms ``` -**CHECK**值是使用Carl Friedrich Gauss在1700年代后期仍在小学时创建的公式计算出来的. +**CHECK**值是使用Carl Friedrich Gauss(高斯)在1700年代后期还在上小学的时候创建的公式计算出来的. - **main()** 的第一个版本使用直接生成 **Stream** 并调用 **sum()** 的方法。我们看到流的好处在于十亿分之一的SZ在没有溢出的情况下处理(我使用较小的数字,因此程序运行时间不长)。使用 **parallel()** 的基本范围操跟快。 + **main()** 的第一个版本使用直接生成 **Stream** 并调用 **sum()** 的方法。我们看到流的好处在于即使SZ为十亿(1_000_000_000)程序也可以很好地处理而没有溢出(为了让程序运行得快一点,我使用了较小的数字)。使用 **parallel()** 的基本范围操作明显更快。 -如果使用**iterate()**来生成序列,则减速是戏剧性的,可能是因为每次生成数字时都必须调用lambda。但是如果我们尝试并行化,那么结果通常比非并行版本花费的时间更长,但是当**SZ**超过一百万时,它也会耗尽内存(在某些机器上)。当然,当你可以使用**range()**时,你不会使用**iterate()**,但如果你生成的东西不是简单的序列,你必须使用**iterate()**。应用**parallel()**是一个合理的尝试,但会产生令人惊讶的结果。我们将在后面的部分中探讨内存限制的原因,但我们可以对流并行算法进行初步观察: +如果使用**iterate()**来生成序列,则减速是相当明显的,可能是因为每次生成数字时都必须调用lambda。但是如果我们尝试并行化,当**SZ**超过一百万时,结果不仅比非并行版本花费的时间更长,而且也会耗尽内存(在某些机器上)。当然,当你可以使用**range()**时,你不会使用**iterate()**,但如果你生成的东西不是简单的序列,你必须使用**iterate()**。应用**parallel()**是一个合理的尝试,但会产生令人惊讶的结果。我们将在后面的部分中探讨内存限制的原因,但我们可以对流并行算法进行初步观察: - 流并行性将输入数据分成多个部分,因此算法可以应用于那些单独的部分。 -- 阵列分割成本低廉,均匀且具有完美的分裂知识。 -- 链接列表没有这些属性;“拆分”一个链表仅仅意味着把它分成“第一元素”和“其余列表”,这相对无用。 -- 无状态生成器的行为类似于数组;使用上述范围是无可争议的。 +- 数组分割成本低,分割均匀且对分割的大小有着完美的掌控。 +- 链表没有这些属性;“拆分”一个链表仅仅意味着把它分成“第一元素”和“其余元素”,这相对无用。 +- 无状态生成器的行为类似于数组;上面使用的 **range()** 就是无状态的。 - 迭代生成器的行为类似于链表; **iterate()** 是一个迭代生成器。 -现在让我们尝试通过在数组中填充值来填充数组来解决问题。因为数组只分配了一次,所以我们不太可能遇到垃圾收集时序问题。 +现在让我们尝试通过在数组中填充值并对数组求和来解决问题。因为数组只分配了一次,所以我们不太可能遇到垃圾收集时序问题。 首先我们将尝试一个充满原始**long**的数组: @@ -453,9 +453,9 @@ Basic Sum: 106ms parallelPrefix: 265ms ``` -第一个限制是内存大小;因为数组是预先分配的,所以我们不能创建几乎与以前版本一样大的任何东西。并行化可以加快速度,甚至比使用 **basicSum()** 循环更快。有趣的是, **Arrays.parallelPrefix()** 似乎实际上减慢了速度。但是,这些技术中的任何一种在其他条件下都可能更有用 - 这就是为什么你不能做出任何确定性的声明,除了“你必须尝试一下”。” +第一个限制是内存大小;因为数组是预先分配的,所以我们不能创建几乎与以前版本一样大的任何东西。并行化可以加快速度,甚至比使用 **basicSum()** 循环更快。有趣的是, **Arrays.parallelPrefix()** 似乎实际上减慢了速度。但是,这些技术中的任何一种在其他条件下都可能更有用 - 这就是为什么你不能做出任何确定性的声明,除了“你必须尝试一下”。 -最后,考虑使用包装类**Long**的效果: +最后,考虑使用包装类**Long**的效果: ```java // concurrent/Summing3.java @@ -532,9 +532,9 @@ Long Parallel: 1014ms 它比非parallel()版本略快,但并不显着。 -这种时间增加的一个重要原因是处理器内存缓存。使用**Summing2.java**中的原始**long**,数组**la**是连续的内存。处理器可以更容易地预测该阵列的使用,并使缓存充满下一个需要的阵列元素。访问缓存比访问主内存快得多。似乎 **Long parallelPrefix** 计算受到影响,因为它为每个计算读取两个数组元素,并将结果写回到数组中,并且每个都为**Long**生成一个超出缓存的引用。 +导致时间增加的一个重要原因是处理器内存缓存。使用**Summing2.java**中的原始**long**,数组**la**是连续的内存。处理器可以更容易地预测该阵列的使用,并使缓存充满下一个需要的阵列元素。访问缓存比访问主内存快得多。似乎 **Long parallelPrefix** 计算受到影响,因为它为每个计算读取两个数组元素,并将结果写回到数组中,并且每个都为**Long**生成一个超出缓存的引用。 -使用**Summing3.java**和**Summing4.java**,**aL**是一个**Long**数组,它不是一个连续的数据数组,而是一个连续的**Long**对象引用数组。尽管该数组可能会在缓存中出现,但指向的对象几乎总是超出缓存。 +使用**Summing3.java**和**Summing4.java**,**aL**是一个**Long**数组,它不是一个连续的数据数组,而是一个连续的**Long**对象引用数组。尽管该数组可能会在缓存中出现,但指向的对象几乎总是不在缓存中。 这些示例使用不同的SZ值来显示内存限制。 @@ -552,13 +552,13 @@ Long Basic Sum: 21ms Long parallelPrefix: 3287ms Long Parallel: 1008ms** -虽然Java 8的各种内置“并行”工具非常棒,但我认为它们被视为神奇的灵丹妙药:“只需添加parallel()并且它会更快!”我希望我已经开始表明情况并非所有都是如此,并且盲目地应用内置的“并行”操作有时甚至会使运行速度明显变慢。 +虽然Java 8的各种内置“并行”工具非常棒,但我认为它们被视为神奇的灵丹妙药:“只需添加parallel()并且它会更快!” 我希望我已经开始表明情况并非所有都是如此,并且盲目地应用内置的“并行”操作有时甚至会使运行速度明显变慢。 - parallel()/limit()交点 -使用parallel()时会有更复杂的问题。从其他语言中吸取的流是围绕无限流模型设计的。如果你拥有有限数量的元素,则可以使用集合以及为有限大小的集合设计的关联算法。如果你使用无限流,则使用针对流优化的算法。 +使用**parallel()**时会有更复杂的问题。从其他语言中吸取的流机制被设计为大约是一个无限的流模型。如果你拥有有限数量的元素,则可以使用集合以及为有限大小的集合设计的关联算法。如果你使用无限流,则使用针对流优化的算法。 -Java 8将两者合并起来。例如,**Collections**没有内置的**map()**操作。在Collection和Map中唯一类似流的批处理操作是**forEach()**。如果要执行**map()**和**reduce()**等操作,必须首先将Collection转换为存在这些操作的Stream: +Java 8将两者合并起来。例如,**Collections**没有内置的**map()**操作。在**Collection**和**Map**中唯一类似流的批处理操作是**forEach()**。如果要执行**map()**和**reduce()**等操作,必须首先将**Collection**转换为存在这些操作的**Stream**: ```java // concurrent/CollectionIntoStream.java @@ -584,6 +584,7 @@ public class CollectionIntoStream { 输出结果: ``` +btpen pccux szgvg meinn @@ -596,9 +597,9 @@ bynxt :PENCUXGVGINNLOZVEWPPCPOALJLNXT ``` -**Collection**确实有一些批处理操作,如**removeAll()**,**removeIf()**和**retainAll()**,但这些都是破坏性的操作.**ConcurrentHashMap**对**forEachand**和**reduce**操作有特别广泛的支持。 +**Collection**确实有一些批处理操作,如**removeAll()**,**removeIf()**和**retainAll()**,但这些都是破坏性的操作。**ConcurrentHashMap**对**forEach**和**reduce**操作有特别广泛的支持。 -在许多情况下,只在集合上调用**stream()**或者**parallelStream()**没有问题。但是,有时将**Stream**与**Collection**混合会产生意外。这是一个有趣的难题: +在许多情况下,只在集合上调用**stream()**或者**parallelStream()**没有问题。但是,有时将**Stream**与**Collection**混合会产生意想不到的结果。这是一个有趣的难题: ```java // concurrent/ParallelStreamPuzzle.java @@ -609,6 +610,7 @@ public class ParallelStreamPuzzle { static class IntGenerator implements Supplier { private int current = 0; + @Override public Integer get() { return current++; } @@ -630,7 +632,7 @@ public class ParallelStreamPuzzle { **[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]** 每次。但是包含了parallel(),它看起来像一个随机数生成器,带有输出(从一次运行到下一次运行不同),如: **[0, 3, 6, 8, 11, 14, 17, 20, 23, 26]** -这样一个简单的程序怎么会这么破碎呢?让我们考虑一下我们在这里要实现的目标:“并行生成。”“那意味着什么?一堆线程都在拉动一个生成器,在某种程度上选择一组有限的结果?代码使它看起来很简单,但它转向是一个特别凌乱的问题。 +这样一个简单的程序怎么会如此糟糕呢?让我们考虑一下我们在这里要实现的目标:“并行生成。”那意味着什么?一堆线程都在从一个生成器取值,然后以某种方式选择有限的结果集?代码看起来很简单,但它变成了一个特别棘手的问题。 为了看到它,我们将添加一些仪器。由于我们正在处理线程,因此我们必须将任何跟踪信息捕获到并发数据结构中。在这里我使用**ConcurrentLinkedDeque**: @@ -643,14 +645,15 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.*; import java.nio.file.*; public class ParallelStreamPuzzle2 { - public static final Deque trace = + public static final Deque TRACE = new ConcurrentLinkedDeque<>(); static class IntGenerator implements Supplier { private AtomicInteger current = new AtomicInteger(); - public Integerget() { - trace.add(current.get() + ": " +Thread.currentThread().getName()); + @Override + public Integer get() { + TRACE.add(current.get() + ": " +Thread.currentThread().getName()); return current.getAndIncrement(); } } @@ -660,7 +663,7 @@ public class ParallelStreamPuzzle2 { .parallel() .collect(Collectors.toList()); System.out.println(x); - Files.write(Paths.get("PSP2.txt"), trace); + Files.write(Paths.get("PSP2.txt"), TRACE); } } ``` @@ -699,9 +702,9 @@ current是使用线程安全的 **AtomicInteger** 类定义的,可以防止竞 22: ForkJoinPool.commonPool-worker-110 23: ForkJoinPool.commonPool-worker-1** -这个块大小似乎是内部实现的一部分(尝试使用**limit()**的不同参数来查看不同的块大小)。将**parallel()**与**limit()**结合使用可以预取一串值,作为流输出。 +这个块大小似乎是内部实现的一部分(尝试使用`limit()` 的不同参数来查看不同的块大小)。将`parallel()`与`limit()`结合使用可以预取一串值,作为流输出。 -试着想象一下这里发生了什么:一个流抽象出无限序列,按需生成。当你要求它并行产生流时,你要求所有这些线程尽可能地调用get()。添加limit(),你说“只需要这些。”基本上,当你将parallel()与limit()结合使用时,你要求随机输出 - 这可能对你正在解决的问题很好。但是当你这样做时,你必须明白。这是一个仅限专家的功能,而不是要争辩说“Java弄错了”。 +试着想象一下这里发生了什么:一个流抽象出无限序列,按需生成。当你要求它并行产生流时,你要求所有这些线程尽可能地调用`get()`。添加`limit()`,你说“只需要这些。”基本上,当你为了随机输出而选择将`parallel()`与`limit()`结合使用时,这种方法可能对你正在解决的问题有效。但是当你这样做时,你必须明白。这是一个仅限专家的功能,而不是要争辩说“Java弄错了”。 什么是更合理的方法来解决问题?好吧,如果你想生成一个int流,你可以使用IntStream.range(),如下所示: @@ -742,15 +745,15 @@ public class ParallelStreamPuzzle3 { 为了表明**parallel()**确实有效,我添加了一个对**peek()**的调用,这是一个主要用于调试的流函数:它从流中提取一个值并执行某些操作但不影响从流向下传递的元素。注意这会干扰线程行为,但我只是尝试在这里做一些事情,而不是实际调试任何东西。 -你还可以看到boxed()的添加,它接受int流并将其转换为Integer流。 +你还可以看到**boxed()**的添加,它接受**int**流并将其转换为**Integer**流。 现在我们得到多个线程产生不同的值,但它只产生10个请求的值,而不是1024个产生10个值。 -它更快吗?一个更好的问题是:什么时候开始有意义?当然不是这么小的一套;上下文切换的代价远远超过并行性的任何加速。当一个简单的数字序列并行生成时,有点难以想象。如果你使用昂贵的产品,它可能有意义 - 但这都是猜测。唯一知道的是通过测试。记住这句格言:“首先制作它,然后快速制作 - 但只有你必须这样做。”**parallel()**和**limit()**仅供专家使用(并且要清楚,我不认为自己是这里的专家)。 +它更快吗?一个更好的问题是:什么时候开始有意义?当然不是这么小的一套;上下文切换的代价远远超过并行性的任何加速。很难想象什么时候用并行生成一个简单的数字序列会有意义。如果你要生成的东西需要很高的成本,它可能有意义 - 但这都是猜测。只有通过测试我们才能知道用并行是否有效。记住这句格言:“首先使它工作,然后使它更快地工作 - 只有当你必须这样做时。”**parallel()**和**limit()**仅供专家使用(把话说在前面,我不认为自己是这里的专家)。 - 并行流只看起来很容易 -实际上,在许多情况下,并行流确实可以毫不费力地更快地产生结果。但正如你所见,只需将**parallel()**打到你的Stream操作上并不一定是安全的事情。在使用**parallel()**之前,你必须了解并行性如何帮助或损害你的操作。有个错误认识是认为并行性总是一个好主意。事实上并不是。Stream意味着你不需要重写所有代码以便并行运行它。流什么都不做的是取代理解并行性如何工作的需要,以及它是否有助于实现你的目标。 +实际上,在许多情况下,并行流确实可以毫不费力地更快地产生结果。但正如你所见,仅仅将**parallel()**加到你的Stream操作上并不一定是安全的事情。在使用**parallel()**之前,你必须了解并行性如何帮助或损害你的操作。一个基本认知错误就是认为使用并行性总是一个好主意。事实上并不是。Stream意味着你不需要重写所有代码便可以并行运行它。但是流的出现并不意味着你可以不用理解并行的原理以及不用考虑并行是否真的有助于实现你的目标。 ## 创建和运行任务 @@ -762,7 +765,7 @@ Java并发的历史始于非常原始和有问题的机制,并且充满了各 在Java的早期版本中,你通过直接创建自己的Thread对象来使用线程,甚至将它们子类化以创建你自己的特定“任务线程”对象。你手动调用了构造函数并自己启动了线程。 -创建所有这些线程的开销变得非常重要,现在不鼓励采用实际操作方法。在Java 5中,添加了类来为你处理线程池。你可以将任务创建为单独的类型,然后将其交给ExecutorService以运行该任务,而不是为每种不同类型的任务创建新的Thread子类型。ExecutorService为你管理线程,并且在运行任务后重新循环线程而不是丢弃线程。 +创建所有这些线程的开销变得非常重要,现在不鼓励采用手动操作方法。在Java 5中,添加了类来为你处理线程池。你可以将任务创建为单独的类型,然后将其交给ExecutorService以运行该任务,而不是为每种不同类型的任务创建新的Thread子类型。ExecutorService为你管理线程,并且在运行任务后重新循环线程而不是丢弃线程。 首先,我们将创建一个几乎不执行任务的任务。它“sleep”(暂停执行)100毫秒,显示其标识符和正在执行任务的线程的名称,然后完成: @@ -807,11 +810,11 @@ public class Nap { } } ``` -为了消除异常处理的视觉噪声,这被定义为实用程序。第二个构造函数在超时时显示一条消息 +为了消除异常处理的视觉干扰,这被定义为实用程序。第二个构造函数在超时时显示一条消息 对**TimeUnit.MILLISECONDS.sleep()**的调用获取“当前线程”并在参数中将其置于休眠状态,这意味着该线程被挂起。这并不意味着底层处理器停止。操作系统将其切换到其他任务,例如在你的计算机上运行另一个窗口。OS任务管理器定期检查**sleep()**是否超时。当它执行时,线程被“唤醒”并给予更多处理时间。 -你可以看到**sleep()**抛出一个已检查的**InterruptedException**;这是原始Java设计中的一个工件,它通过突然断开它们来终止任务。因为它往往会产生不稳定的状态,所以后来不鼓励终止。但是,我们必须在需要或仍然发生终止的情况下捕获异常。 +你可以看到**sleep()**抛出一个受检的**InterruptedException**;这是原始Java设计中的一个工件,它通过突然断开它们来终止任务。因为它往往会产生不稳定的状态,所以后来不鼓励终止。但是,我们必须在需要或仍然发生终止的情况下捕获异常。 要执行任务,我们将从最简单的方法--SingleThreadExecutor开始: @@ -872,7 +875,7 @@ NapTask[9] pool-1-thread-1 请注意,main()中线程的名称是main,并且只有一个其他线程pool-1-thread-1。此外,交错输出显示两个线程确实同时运行。 -如果你只是调用exec.shutdown(),程序将完成所有任务。也就是说,虽然不需要(!exec.isTerminated())。 +如果你只是调用exec.shutdown(),程序将完成所有任务。也就是说,不需要**while(!exec.isTerminated())**。 ```java // concurrent/SingleThreadExecutor2.java @@ -968,7 +971,7 @@ NapTask[6] pool-1-thread-7 NapTask[5] pool-1-thread-6 ``` -当你运行这个程序时,你会发现它完成得更快。这是有道理的,而不是使用相同的线程来顺序运行每个任务,每个任务都有自己的线程,所以它们都并行运行。似乎没有缺点,很难看出为什么有人会使用SingleThreadExecutor。 +当你运行这个程序时,你会发现它完成得更快。这是有道理的,每个任务都有自己的线程,所以它们都并行运行,而不是使用相同的线程来顺序运行每个任务。这似乎没毛病,很难理解为什么有人会使用SingleThreadExecutor。 要理解这个问题,我们需要一个更复杂的任务: @@ -1024,7 +1027,7 @@ public class CachedThreadPool2 { 6 pool-1-thread-7 1000 ``` -输出不是我们所期望的,并且从一次运行到下一次运行会有所不同。问题是所有的任务都试图写入val的单个实例,并且他们正在踩着彼此的脚趾。我们说这样的类不是线程安全的。让我们看看SingleThreadExecutor会发生什么: +输出不是我们所期望的,并且从一次运行到下一次运行会有所不同。问题是所有的任务都试图写入val的单个实例,并且他们正在踩着彼此的脚趾。我们称这样的类是线程不安全的。让我们看看SingleThreadExecutor会发生什么: ```java // concurrent/SingleThreadExecutor3.java @@ -1057,7 +1060,7 @@ public class SingleThreadExecutor3 { 9 pool-1-thread-1 1000 ``` -现在我们每次都得到一致的结果,尽管**InterferingTask**缺乏线程安全性。这是SingleThreadExecutor的主要好处 - 因为它一次运行一个任务,这些任务不会相互干扰,因此强加了线程安全性。这种现象称为线程限制,因为在单线程上运行任务限制了它们的影响。线程限制限制了加速,但可以节省很多困难的调试和重写。 +现在我们每次都得到一致的结果,尽管**InterferingTask**缺乏线程安全性。这是SingleThreadExecutor的主要好处 - 因为它一次运行一个任务,这些任务不会相互干扰,因此强加了线程安全性。这种现象称为线程封闭,因为在单线程上运行任务限制了它们的影响。线程封闭限制了加速,但可以节省很多困难的调试和重写。 - 产生结果 @@ -1164,13 +1167,13 @@ public class Futures { 100 ``` -- [1] 当你的任务尚未完成的**Future**上调用**get()**时,调用会阻塞(等待)直到结果可用。 +- [1] 当你的任务在尚未完成的**Future**上调用**get()**时,调用会阻塞(等待)直到结果可用。 但这意味着,在**CachedThreadPool3.java**中,**Future**似乎是多余的,因为**invokeAll()**甚至在所有任务完成之前都不会返回。但是,这里的Future并不用于延迟结果,而是用于捕获任何可能发生的异常。 还要注意在**CachedThreadPool3.java.get()**中抛出异常,因此**extractResult()**在Stream中执行此提取。 -因为当你调用**get()**时,**Future**会阻塞,所以它只能解决等待任务完成的问题。最终,**Futures**被认为是一种无效的解决方案,现在不鼓励,支持Java 8的**CompletableFuture**,我们将在本章后面探讨。当然,你仍会在遗留库中遇到Futures +因为当你调用**get()**时,**Future**会阻塞,所以它只能解决等待任务完成才暴露问题。最终,**Futures**被认为是一种无效的解决方案,现在不鼓励,我们推荐Java 8的**CompletableFuture**,这将在本章后面探讨。当然,你仍会在遗留库中遇到Futures。 我们可以使用并行Stream以更简单,更优雅的方式解决这个问题: @@ -1208,11 +1211,11 @@ public class CountingStream { 1000 ``` -这不仅更容易理解,我们需要做的就是将**parallel()**插入到其他顺序操作中,然后一切都在同时运行。 +这不仅更容易理解,而且我们需要做的就是将 `parallel()` 插入到其他顺序操作中,然后一切都在同时运行。 - Lambda和方法引用作为任务 -在 `java8` , 你不需要受限于在 `Runnables ` 和 `Callables` 时,使用`lambdas` 和方法引用, 同样也可以通过匹配签名来引用(即,它支持结构一致性)。 所以我们可以将 `notRunnables` 或 `Callables` 的参数传递给`ExecutorService` : +在 **java8** 有了 **lambdas** 和方法引用,你不需要受限于只能使用 **Runnable** 和 **Callable** 。因为 java8 的**lambdas** 和方法引用可以通过匹配方法签名来使用(即,它支持结构一致性),所以我们可以将非 **Runnable** 或 **Callable** 的参数传递给 `ExecutorService` : ```java // concurrent/LambdasAndMethodReferences.java