diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md index 15cdfef..5a82b1e 100644 --- a/docs/book/24-Concurrent-Programming.md +++ b/docs/book/24-Concurrent-Programming.md @@ -19,7 +19,7 @@ 而不是单一的意识流叙事,我们在同时多条故事线进行的间谍小说里。一个间谍在一个特殊的岩石下李璐下微缩胶片,当第二个间谍来取回包裹时,它可能已经被第三个间谍带走了。但是这部特别的小说并没有把事情搞得一团糟;你可以轻松地走到尽头,永远不会弄明白什么。 -构建并发应用程序非常类似于游戏[Jenga](https://en.wikipedia.org/wiki/Jenga),每当你拉出一个块并将其放置在塔上时,一切都会崩溃。每个塔楼和每个应用程序都是独一无二的,有自己的作用。您从构建系统中学到的东西可能不适用于下一个系统。 +构建并发应用程序非常类似于游戏[Jenga](https://en.wikipedia.org/wiki/Jenga),每当你拉出一个块并将其放置在塔上时,一切都会崩溃。每个塔楼和每个应用程序都是独一无二的,有自己的作用。你从构建系统中学到的东西可能不适用于下一个系统。 本章是对并发性的一个非常基本的介绍。虽然我使用了最现代的Java 8工具来演示原理,但这一章远非对该主题的全面处理。我的目标是为你提供足够的基础知识,使你能够解决问题的复杂性和危险性,从而安全的通过这些鲨鱼肆虐的困难水域。 @@ -64,7 +64,7 @@ _并行_ 我开始怀疑是否真的有高度抽象。当编写这些类型的程序时,你永远不会被底层系统和工具屏蔽,甚至关于CPU缓存如何工作的细节。最后,如果你非常小心,你创作的东西在特定的情况下起作用,但它在其他情况下不起作用。有时,区别在于两台机器的配置方式,或者程序的估计负载。这不是Java特有的-它是并发和并行编程的本质。 -您可能会认为[纯函数式](https://en.wikipedia.org/wiki/Purely_functional)语言没有这些限制。实际上,纯函数式语言解决了大量并发问题,所以如果你正在解决一个困难的并发问题,你可以考虑用纯函数语言编写这个部分。但最终,如果你编写一个使用队列的系统,例如,如果它没有正确调整并且输入速率要么没有被正确估计或被限制(并且限制意味着,在不同情况下不同的东西具有不同的影响),该队列将填满并阻塞或溢出。最后,您必须了解所有细节,任何问题都可能会破坏您的系统。这是一种非常不同的编程方式 +你可能会认为[纯函数式](https://en.wikipedia.org/wiki/Purely_functional)语言没有这些限制。实际上,纯函数式语言解决了大量并发问题,所以如果你正在解决一个困难的并发问题,你可以考虑用纯函数语言编写这个部分。但最终,如果你编写一个使用队列的系统,例如,如果它没有正确调整并且输入速率要么没有被正确估计或被限制(并且限制意味着,在不同情况下不同的东西具有不同的影响),该队列将填满并阻塞或溢出。最后,你必须了解所有细节,任何问题都可能会破坏你的系统。这是一种非常不同的编程方式 ### 并发的新定义 @@ -75,14 +75,14 @@ _并行_ 这实际上是一个相当多的声明,所以我将其分解: - 这是一个集合:有许多不同的方法来解决这个问题。这是使定义并发性如此具有挑战性的问题之一,因为技术差别很大 -- 这些是性能技术:就是这样。并发的关键点在于让您的程序运行得更快。在Java中,并发是非常棘手和困难的,所以绝对不要使用它,除非你有一个重大的性能问题 - 即使这样,使用最简单的方法产生你需要的性能,因为并发很快变得无法管理。 -- “减少等待”部分很重要而且微妙。无论(例如)你运行多少个处理器,你只能在等待某个地方时产生结果。如果您发起I/O请求并立即获得结果,没有延迟,因此无需改进。如果您在多个处理器上运行多个任务,并且每个处理器都以满容量运行,并且任何其他任务都没有等待,那么尝试提高吞吐量是没有意义的。并发的唯一形式是如果程序的某些部分被迫等待。等待可以以多种形式出现 - 这解释了为什么存在如此不同的并发方法。 +- 这些是性能技术:就是这样。并发的关键点在于让你的程序运行得更快。在Java中,并发是非常棘手和困难的,所以绝对不要使用它,除非你有一个重大的性能问题 - 即使这样,使用最简单的方法产生你需要的性能,因为并发很快变得无法管理。 +- “减少等待”部分很重要而且微妙。无论(例如)你运行多少个处理器,你只能在等待某个地方时产生结果。如果你发起I/O请求并立即获得结果,没有延迟,因此无需改进。如果你在多个处理器上运行多个任务,并且每个处理器都以满容量运行,并且任何其他任务都没有等待,那么尝试提高吞吐量是没有意义的。并发的唯一形式是如果程序的某些部分被迫等待。等待可以以多种形式出现 - 这解释了为什么存在如此不同的并发方法。 值得强调的是,这个定义的有效性取决于等待这个词。如果没有什么可以等待,那就没有机会了。如果有什么东西在等待,那么就会有很多方法可以加快速度,这取决于多种因素,包括系统运行的配置,你要解决的问题类型以及其他许多问题。 ## 并发的超能力 -想象一下,你是一部科幻电影。您必须在高层建筑中搜索一个精心巧妙地隐藏在建筑物的一千万个房间之一中的单个物品。您进入建筑物并移动走廊。走廊分开了。 +想象一下,你是一部科幻电影。你必须在高层建筑中搜索一个精心巧妙地隐藏在建筑物的一千万个房间之一中的单个物品。你进入建筑物并移动走廊。走廊分开了。 你自己完成这项任务需要一百个生命周期。 @@ -94,36 +94,36 @@ _并行_ 我很想能够说,“你在科幻小说中的超级大国?这就是并发性。“每当你有更多的任务要解决时,它就像分裂两个一样简单。问题是我们用来描述这种现象的任何模型最终都是抽象的 -以下是其中一个漏洞:在理想的世界中,每次克隆自己时,您还会复制硬件处理器来运行该克隆。但当然不会发生这种情况 - 您的机器上可能有四个或八个处理器(通常在写入时)。您可能还有更多,并且仍有许多情况只有一个处理器。在抽象的讨论中,物理处理器的分配方式不仅可以泄漏,甚至可以支配您的决策 +以下是其中一个漏洞:在理想的世界中,每次克隆自己时,你还会复制硬件处理器来运行该克隆。但当然不会发生这种情况 - 你的机器上可能有四个或八个处理器(通常在写入时)。你可能还有更多,并且仍有许多情况只有一个处理器。在抽象的讨论中,物理处理器的分配方式不仅可以泄漏,甚至可以支配你的决策 让我们在科幻电影中改变一些东西。现在当每个克隆搜索者最终到达一扇门时,他们必须敲门并等到有人回答。如果我们每个搜索者有一个处理器,这没有问题 - 处理器只是空闲,直到门被回答。但是如果我们只有8个处理器和数千个搜索者,那么只是因为搜索者恰好是因为处理器闲置了被锁,等待一扇门被接听。相反,我们希望将处理器应用于搜索,在那里它可以做一些真正的工作,因此需要将处理器从一个任务切换到另一个任务的机制。 -许多型号能够有效地隐藏处理器的数量,并允许您假装您的数量非常大。但是有些情况会发生故障的时候,你必须知道处理器的数量,以便你可以解决这个问题。 +许多型号能够有效地隐藏处理器的数量,并允许你假装你的数量非常大。但是有些情况会发生故障的时候,你必须知道处理器的数量,以便你可以解决这个问题。 -其中一个最大的影响取决于您是单个处理器还是多个处理器。如果你只有一个处理器,那么任务切换的成本也由该处理器承担,将并发技术应用于你的系统会使它运行得更慢。 +其中一个最大的影响取决于你是单个处理器还是多个处理器。如果你只有一个处理器,那么任务切换的成本也由该处理器承担,将并发技术应用于你的系统会使它运行得更慢。 -这可能会让您决定,在单个处理器的情况下,编写并发代码时没有意义。然而,有些情况下,并发模型会产生更简单的代码,实际上值得让它运行得更慢以实现。 +这可能会让你决定,在单个处理器的情况下,编写并发代码时没有意义。然而,有些情况下,并发模型会产生更简单的代码,实际上值得让它运行得更慢以实现。 在克隆体敲门等待的情况下,即使单处理器系统也能从并发中受益,因为它可以从等待(阻塞)的任务切换到准备好的任务。但是如果所有任务都可以一直运行那么切换的成本会降低一切,在这种情况下,如果你有多个进程,并发通常只会有意义。 在接听电话的客户服务部门,你只有一定数量的人,但是你可以拨打很多电话。那些人(处理器)必须一次拨打一个电话,直到完成电话和额外的电话必须排队。 -在“鞋匠和精灵”的童话故事中,鞋匠做了很多工作,当他睡着时,一群精灵来为他制作鞋子。这里的工作是分布式的,但即使使用大量的物理处理器,在制造鞋子的某些部件时会产生限制 - 例如,如果鞋底需要制作鞋子,这会限制制鞋的速度并改变您设计解决方案的方式。 +在“鞋匠和精灵”的童话故事中,鞋匠做了很多工作,当他睡着时,一群精灵来为他制作鞋子。这里的工作是分布式的,但即使使用大量的物理处理器,在制造鞋子的某些部件时会产生限制 - 例如,如果鞋底需要制作鞋子,这会限制制鞋的速度并改变你设计解决方案的方式。 -因此,您尝试解决的问题驱动解决方案的设计。打破一个“独立运行”问题的高级[原文:lovely ]抽象,然后就是实际发生的现实。物理现实不断侵入和震撼,这种抽象。 +因此,你尝试解决的问题驱动解决方案的设计。打破一个“独立运行”问题的高级[原文:lovely ]抽象,然后就是实际发生的现实。物理现实不断侵入和震撼,这种抽象。 这只是问题的一部分。考虑一个制作蛋糕的工厂。我们不知何故在工人中分发了蛋糕制作任务,但是现在是时候让工人把蛋糕放在盒子里了。那里有一个盒子,准备收到蛋糕。但是,在工人将蛋糕放入盒子之前,另一名工人投入并将蛋糕放入盒子中!我们的工人已经把蛋糕放进去了,然后就开始了!这两个蛋糕被砸碎并毁了。这是常见的“共享内存”问题,产生我们称之为竞争条件的问题,其结果取决于哪个工作人员可以首先在框中获取蛋糕(通常使用锁定机制来解决问题,因此一个工作人员可以先抓住框并防止蛋糕砸)。 当“同时”执行的任务相互干扰时,会出现问题。他可以以如此微妙和偶然的方式发生,可能公平地说,并发性“可以说是确定性的,但实际上是非确定性的。”也就是说,你可以假设编写通过维护和代码检查正常工作的并发程序。然而,在实践中,编写仅看起来可行的并发程序更为常见,但是在适当的条件下,将会失败。这些情况可能会发生,或者很少发生,你在测试期间从未看到它们。实际上,编写测试代码通常无法为并发程序生成故障条件。由此产生的失败只会偶尔发生,因此它们以客户投诉的形式出现。 这是推动并发的最强有力的论据之一:如果你忽略它,你可能会被咬。 -因此,并发似乎充满了危险,如果这让你有点害怕,这可能是一件好事。尽管Java 8在并发性方面做出了很大改进,但仍然没有像编译时验证或检查异常那样的安全网来告诉您何时出现错误。通过并发,您可以自己动手,只有知识渊博,可疑和积极,才能用Java编写可靠的并发代码。 +因此,并发似乎充满了危险,如果这让你有点害怕,这可能是一件好事。尽管Java 8在并发性方面做出了很大改进,但仍然没有像编译时验证或检查异常那样的安全网来告诉你何时出现错误。通过并发,你可以自己动手,只有知识渊博,可疑和积极,才能用Java编写可靠的并发代码。 ## 并发为速度而生 -在听说并发编程的问题之后,你可能会想知道它是否值得这么麻烦。答案是“不,除非你的程序运行速度不够快。”并且在决定它没有之前你会想要仔细思考。不要随便跳进并发编程的悲痛之中。如果有一种方法可以在更快的机器上运行您的程序,或者如果您可以对其进行分析并发现瓶颈并在该位置交换更快的算法,那么请执行此操作。只有在显然没有其他选择时才开始使用并发,然后仅在孤立的地方。 +在听说并发编程的问题之后,你可能会想知道它是否值得这么麻烦。答案是“不,除非你的程序运行速度不够快。”并且在决定它没有之前你会想要仔细思考。不要随便跳进并发编程的悲痛之中。如果有一种方法可以在更快的机器上运行你的程序,或者如果你可以对其进行分析并发现瓶颈并在该位置交换更快的算法,那么请执行此操作。只有在显然没有其他选择时才开始使用并发,然后仅在孤立的地方。 速度问题一开始听起来很简单:如果你想要一个程序运行得更快,将其分解成碎片并在一个单独的处理器上运行每个部分。由于我们能够提高时钟速度流(至少对于传统芯片),速度的提高是出现在多核处理器的形式而不是更快的芯片。为了使你的程序运行得更快,你必须学习利用那些超级处理器,这是并发性给你的一个建议。 @@ -139,11 +139,11 @@ _并行_ 有些人甚至提倡将进程作为并发的唯一合理方法[^1],但不幸的是,通常存在数量和开销限制,以防止它们在并发频谱中的适用性(最终你习惯了标准的并发性克制,“这种方法适用于一些情况但不适用于其他情况”) -一些编程语言旨在将并发任务彼此隔离。这些通常被称为_函数式语言_,其中每个函数调用不产生其他影响(因此不能与其他函数干涉),因此可以作为独立的任务来驱动。Erlang就是这样一种语言,它包括一个任务与另一个任务进行通信的安全机制。如果您发现程序的一部分必须大量使用并发性并且您在尝试构建该部分时遇到了过多的问题,那么您可能会考虑使用专用并发语言创建程序的那一部分。 +一些编程语言旨在将并发任务彼此隔离。这些通常被称为_函数式语言_,其中每个函数调用不产生其他影响(因此不能与其他函数干涉),因此可以作为独立的任务来驱动。Erlang就是这样一种语言,它包括一个任务与另一个任务进行通信的安全机制。如果你发现程序的一部分必须大量使用并发性并且你在尝试构建该部分时遇到了过多的问题,那么你可能会考虑使用专用并发语言创建程序的那一部分。 Java采用了更传统的方法[^2],即在顺序语言之上添加对线程的支持而不是在多任务操作系统中分配外部进程,线程在执行程序所代表的单个进程中创建任务交换。 -并发性会带来成本,包括复杂性成本,但可以通过程序设计,资源平衡和用户便利性的改进来抵消。通常,并发性使您能够创建更加松散耦合的设计;否则,您的代码部分将被迫明确标注通常由并发处理的操作。 +并发性会带来成本,包括复杂性成本,但可以通过程序设计,资源平衡和用户便利性的改进来抵消。通常,并发性使你能够创建更加松散耦合的设计;否则,你的代码部分将被迫明确标注通常由并发处理的操作。 ## 四句格言 @@ -167,7 +167,7 @@ Java采用了更传统的方法[^2],即在顺序语言之上添加对线程的 证明并发性的唯一因素是速度。如果你的程序运行速度不够快 - 在这里要小心,因为只是希望它运行得更快是不合理的 - 首先应用一个分析器(参见代码校验章中分析和优化)来发现你是否可以执行其他一些优化。 -如果您被迫进行并发,请采取最简单,最安全的方法来解决问题。使用众所周知的库并尽可能少地编写自己的代码。有了并发性,就没有“太简单了”。自负才是你的敌人。 +如果你被迫进行并发,请采取最简单,最安全的方法来解决问题。使用众所周知的库并尽可能少地编写自己的代码。有了并发性,就没有“太简单了”。自负才是你的敌人。 ### 2.没有什么是真的,一切可能都有问题 @@ -175,41 +175,41 @@ Java采用了更传统的方法[^2],即在顺序语言之上添加对线程的 在并发领域,有些事情可能是真的而有些事情却不是,你必须认为没有什么是真的。你必须质疑一切。即使将变量设置为某个值也可能或者可能不会按预期的方式工作,并且从那里开始走下坡路。我已经很熟悉的东西,认为它显然有效但实际上并没有。 -在非并发程序中你可以忽略的各种事情突然变得非常重要。例如,您必须知道处理器缓存以及保持本地缓存与主内存一致的问题。您必须了解对象构造的深度复杂性,以便您的构造对象不会意外地将数据暴露给其他线程进行更改。问题还有很多。 +在非并发程序中你可以忽略的各种事情突然变得非常重要。例如,你必须知道处理器缓存以及保持本地缓存与主内存一致的问题。你必须了解对象构造的深度复杂性,以便你的构造对象不会意外地将数据暴露给其他线程进行更改。问题还有很多。 -虽然这些主题太复杂,无法为您提供本章的专业知识(再次参见Java Concurrency in Practice),但您必须了解它们。 +虽然这些主题太复杂,无法为你提供本章的专业知识(再次参见Java Concurrency in Practice),但你必须了解它们。 ### 3.它起作用,并不意味着它没有问题 -您可以轻松编写一个似乎可以工作,但实际上是有问题的并发程序,并且该问题仅在最极限的条件下显示出来 - 在您部署程序后不可避免地会出现用户问题。 +我们很容易编写出一个看似完美实则有问题的并发程序,并且往往问题直在极端情况下才暴露出来 - 在程序部署后不可避免地会出现用户问题。 - 你不能证明并发程序是正确的,你只能(有时)证明它是不正确的。 - 大多数情况下你甚至不能这样做:如果它有问题,你可能无法检测到它。 -- 您通常不能编写有用的测试,因此您必须依靠代码检查结合深入的并发知识来发现错误。 +- 你通常不能编写有用的测试,因此你必须依靠代码检查结合深入的并发知识来发现错误。 - 即使是有效的程序也只能在其设计参数下工作。当超出这些设计参数时,大多数并发程序会以某种方式失败。 在其他Java主题中,我们培养了一种感觉-决定论。一切都按照语言的承诺(或隐含)进行,这是令人欣慰和期待的 - 毕竟,编程语言的目的是让机器做我们想要的。从确定性编程的世界进入并发编程领域,我们遇到了一种称为[Dunning-Kruger](https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect)效应的认知偏差,可以概括为“你知道的越多,你认为你知道得越多。”这意味着“......相对不熟练的人拥有着虚幻的优越感,错误地评估他们的能力远高于实际。 -我自己的经验是,无论你是多么确定你的代码是线程安全的,它可能已经无效了。你可以很容易地了解所有的问题,然后几个月或几年后你会发现一些概念让你意识到你编写的大多数内容实际上都容易受到并发错误的影响。当某些内容不正确时,编译器不会告诉您。为了使它正确,你必须在研究代码时掌握前脑的所有并发问题。 +我自己的经验是,无论你是多么确定你的代码是线程安全的,它可能已经无效了。你可以很容易地了解所有的问题,然后几个月或几年后你会发现一些概念让你意识到你编写的大多数内容实际上都容易受到并发错误的影响。当某些内容不正确时,编译器不会告诉你。为了使它正确,你必须在研究代码时掌握前脑的所有并发问题。 在Java的所有非并发领域,“没有明显的错误和没有明显的编译错误”似乎意味着一切都好。对于并发,它没有任何意义。你可以在这个情况下做的最糟糕的事情是“自信”。 ### 4.你必须仍然理解 -在格言1-3之后,您可能会对并发性感到害怕,并且认为,“到目前为止,我已经避免了它,也许我可以继续保留它。 +在格言1-3之后,你可能会对并发性感到害怕,并且认为,“到目前为止,我已经避免了它,也许我可以继续保留它。 -这是一种理性的反应。您可能知道其他编程语言更好地设计用于构建并发程序 - 甚至是在JVM上运行的程序(从而提供与Java的轻松通信),例如Clojure或Scala。为什么不用这些语言编写并发部分并将Java用于其他所有部分呢? +这是一种理性的反应。你可能知道其他编程语言更好地设计用于构建并发程序 - 甚至是在JVM上运行的程序(从而提供与Java的轻松通信),例如Clojure或Scala。为什么不用这些语言编写并发部分并将Java用于其他所有部分呢? 唉,你不能轻易逃脱: - 即使你从未明确地创建一个线程,你可能使用的框架 - 例如,Swing图形用户界面(GUI)库,或者像**Timer** clas那样简单的东西。 -- 这是最糟糕的事情:当您创建组件时,您必须假设这些组件可能在多线程环境中重用。即使你的解决方案是放弃并声明你的组件“不是线程安全的”,你仍然必须知道这样的声明是重要的,它是什么意思? +- 这是最糟糕的事情:当你创建组件时,你必须假设这些组件可能在多线程环境中重用。即使你的解决方案是放弃并声明你的组件“不是线程安全的”,你仍然必须知道这样的声明是重要的,它是什么意思? 人们有时会认为并发性太难,不能包含在介绍该语言的书中。他们认为并发是一个可以独立对待的独立主题,并且它在日常编程中出现的少数情况(例如图形用户界面)可以用特殊的习语来处理。如果你可以避免它,为什么要介绍这样的复杂的主题。 -唉,如果只是这样的话,那就太好了。但不幸的是,您无法选择何时在Java程序中出现线程。仅仅你从未写过自己的线程,并不意味着你可以避免编写线程代码。例如,Web系统是最常见的Java应用程序之一,本质上是多线程的Web服务器通常包含多个处理器,而并行性是利用这些处理器的理想方式。就像这样的系统看起来那么简单,你必须理解并发才能正确地编写它。 +唉,如果只是这样的话,那就太好了。但不幸的是,你无法选择何时在Java程序中出现线程。仅仅你从未写过自己的线程,并不意味着你可以避免编写线程代码。例如,Web系统是最常见的Java应用程序之一,本质上是多线程的Web服务器通常包含多个处理器,而并行性是利用这些处理器的理想方式。就像这样的系统看起来那么简单,你必须理解并发才能正确地编写它。 -Java是一种多线程语言,如果您了解它们是否存在并发问题。因此,有许多Java程序正在使用中,或者只是偶然工作,或者大部分时间工作并且不时地发生问题,因为。有时这种问题是相对良性的,但有时它意味着丢失有价值的数据,如果你没有意识到并发问题,你最终可能会把问题放在其他地方而不是你的代码中。如果将程序移动到多处理器系统,则可以暴露或放大这些类型的问题。基本上,了解并发性使您意识到正确的程序可能会表现出错误的行为。 +Java是一种多线程语言,如果你了解它们是否存在并发问题。因此,有许多Java程序正在使用中,或者只是偶然工作,或者大部分时间工作并且不时地发生问题,因为。有时这种问题是相对良性的,但有时它意味着丢失有价值的数据,如果你没有意识到并发问题,你最终可能会把问题放在其他地方而不是你的代码中。如果将程序移动到多处理器系统,则可以暴露或放大这些类型的问题。基本上,了解并发性使你意识到正确的程序可能会表现出错误的行为。 ## 残酷的真相 @@ -220,13 +220,13 @@ Java是一种多线程语言,如果您了解它们是否存在并发问题。 有了这种根本性的人类变化,看到许多破坏和失败的实验并不令人惊讶。实际上,进化依赖于无数的实验,其中大多数都失败了。这些实验是向前发展的必要条件 -Java是在充满自信,热情和睿智的氛围中创建的。在发明一种编程语言时,很容易就像语言的初始可塑性会持续存在一样,你可以把某些东西拿出来,如果不能解决问题,那么就修复它。编程语言以这种方式是独一无二的 - 它们经历了类似水的改变:气态,液态和最终的固态。在气体相位期间,灵活性似乎是无限的,并且很容易认为它总是那样。一旦人们开始使用您的语言,变化就会变得更加严重,环境变得更加粘稠。语言设计的过程本身就是一门艺术。 +Java是在充满自信,热情和睿智的氛围中创建的。在发明一种编程语言时,很容易就像语言的初始可塑性会持续存在一样,你可以把某些东西拿出来,如果不能解决问题,那么就修复它。编程语言以这种方式是独一无二的 - 它们经历了类似水的改变:气态,液态和最终的固态。在气体相位期间,灵活性似乎是无限的,并且很容易认为它总是那样。一旦人们开始使用你的语言,变化就会变得更加严重,环境变得更加粘稠。语言设计的过程本身就是一门艺术。 紧迫感来自互联网的最初兴起。它似乎是一场比赛,第一个通过起跑线的人将“获胜”(事实上,Java,JavaScript和PHP等语言的流行程度可以证明这一点)。唉,通过匆忙设计语言而产生的认知负荷和技术债务最终会赶上我们。 [Turing completeness](https://en.wikipedia.org/wiki/Turing_completeness)是不足够的;语言需要更多的东西:它们必须能够创造性地表达,而不是用不必要的东西来衡量我们。解放我们的心理能力只是为了扭转并再次陷入困境,这是毫无意义的。我承认,尽管存在这些问题,我们已经完成了令人惊奇的事情,但我也知道如果没有这些问题我们能做得更多。 -热情使原始Java设计师因为看起来有必要而投入功能。信心(以及原始语言的气味)让他们认为任何问题都可以解决。在时间轴的某个地方,有人认为任何加入Java的东西是固定的和永久性的 - 这是非常有信心,相信第一个决定永远是正确的,因此我们看到Java的体系中充斥着糟糕的决策。其中一些决定最终没有什么后果;例如,您可以告诉人们不要使用Vector,但保留了对之前版本的支持。 +热情使原始Java设计师因为看起来有必要而投入功能。信心(以及原始语言的气味)让他们认为任何问题都可以解决。在时间轴的某个地方,有人认为任何加入Java的东西是固定的和永久性的 - 这是非常有信心,相信第一个决定永远是正确的,因此我们看到Java的体系中充斥着糟糕的决策。其中一些决定最终没有什么后果;例如,你可以告诉人们不要使用Vector,但保留了对之前版本的支持。 线程包含在Java 1.0中。当然,并发性是影响语言远角的基本语言设计决策,很难想象以后添加它。公平地说,当时并不清楚基本的并发性是多少。像C这样的其他语言能够将线程视为一个附加功能,因此Java设计师也纷纷效仿,包括一个Thread类和必要的JVM支持(这比你想象的要复杂得多)。 @@ -240,36 +240,36 @@ Java实验告诉我们,结果是悄然灾难性的。程序员很容易陷入 尽管有这些基本的不可修复的缺陷,但令人印象深刻的是它还有多远。Java的后续版本添加了库,以便在使用并发时提升抽象级别。事实上,我根本不会想到有可能在Java 8中进行改进:并行流和**CompletableFutures** - 这是惊人的史诗般的变化,我会惊奇地重复的查看它[^3]。 -这些改进非常有用,我们将在本章重点介绍并行流和**CompletableFutures**。虽然它们可以大大简化您对并发和后续代码的思考方式,但基本问题仍然存在:由于Java的原始设计,代码的所有部分仍然容易受到攻击,您仍然必须理解这些复杂和微妙的问题。Java中的线程绝不是简单或安全的;那种经历必须降级为另一种更新的语言。 +这些改进非常有用,我们将在本章重点介绍并行流和**CompletableFutures**。虽然它们可以大大简化你对并发和后续代码的思考方式,但基本问题仍然存在:由于Java的原始设计,代码的所有部分仍然容易受到攻击,你仍然必须理解这些复杂和微妙的问题。Java中的线程绝不是简单或安全的;那种经历必须降级为另一种更新的语言。 ## 本章其余部分 -这是我们将在本章的其余部分介绍的内容。请记住,本章的重点是使用最新的高级Java并发结构。使用这些使得您的生活比旧的替代品更加轻松。但是,您仍会在遗留代码中遇到一些低级工具。有时,你可能会被迫自己使用其中的一些。附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md)包含一些更原始的Java并发元素的介绍。 +这是我们将在本章的其余部分介绍的内容。请记住,本章的重点是使用最新的高级Java并发结构。使用这些使得你的生活比旧的替代品更加轻松。但是,你仍会在遗留代码中遇到一些低级工具。有时,你可能会被迫自己使用其中的一些。附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md)包含一些更原始的Java并发元素的介绍。 - Parallel Streams(并发流) -到目前为止,我已经强调了Java 8 Streams提供的改进语法。现在您对该语法(作为一个粉丝,我希望)感到满意,您可以获得额外的好处:您可以通过简单地将parallel()添加到表达式来并行化流。这是一种简单,强大,坦率地说是利用多处理器的惊人方式 +到目前为止,我已经强调了Java 8 Streams提供的改进语法。现在你对该语法(作为一个粉丝,我希望)感到满意,你可以获得额外的好处:你可以通过简单地将parallel()添加到表达式来并行化流。这是一种简单,强大,坦率地说是利用多处理器的惊人方式 添加parallel()来提高速度似乎是微不足道的,但是,唉,它就像你刚刚在[残酷的真相](#The-Brutal-Truth)中学到的那样简单。我将演示并解释一些盲目添加parallel()到Stream表达式的缺陷。 - 创建和运行任务 -任务是一段可以独立运行的代码。为了解释创建和运行任务的一些基础知识,本节介绍了一种比并行流或CompletableFutures:Executor更复杂的机制。执行者管理一些低级Thread对象(Java中最原始的并发形式)。您创建一个任务,然后将其交给Executorto运行。 +任务是一段可以独立运行的代码。为了解释创建和运行任务的一些基础知识,本节介绍了一种比并行流或CompletableFutures:Executor更复杂的机制。执行者管理一些低级Thread对象(Java中最原始的并发形式)。你创建一个任务,然后将其交给Executorto运行。 有多种类型的Executor用于不同的目的。在这里,我们将展示规范形式,代表创建和运行任务的最简单和最佳方法。 - 终止长时间运行的任务 任务独立运行,因此需要一种机制来关闭它们。典型的方法使用了一个标志,这引入了共享内存的问题,我们将使用Java的“Atomic”库来回避它。 - Completable Futures -当您将衣服带到干洗店时,他们会给您一张收据。你继续完成其他任务,最终你的衣服很干净,你可以拿起它。收据是您与干洗店在后台执行的任务的连接。这是Java 5中引入的Future的方法。 +当你将衣服带到干洗店时,他们会给你一张收据。你继续完成其他任务,最终你的衣服很干净,你可以拿起它。收据是你与干洗店在后台执行的任务的连接。这是Java 5中引入的Future的方法。 Future比以前的方法更方便,但你仍然必须出现并用收据取出干洗,等待任务没有完成。对于一系列操作,Futures并没有真正帮助那么多。 -Java 8 CompletableFuture是一个更好的解决方案:它允许您将操作链接在一起,因此您不必将代码写入接口排序操作。有了CompletableFuture完美的结合,就可以更容易地做出“采购原料,组合成分,烹饪食物,提供食物,清理菜肴,储存菜肴”等一系列链式操作。 +Java 8 CompletableFuture是一个更好的解决方案:它允许你将操作链接在一起,因此你不必将代码写入接口排序操作。有了CompletableFuture完美的结合,就可以更容易地做出“采购原料,组合成分,烹饪食物,提供食物,清理菜肴,储存菜肴”等一系列链式操作。 - 死锁 某些任务必须去**等待 - 阻塞**来获得其他任务的结果。被阻止的任务有可能等待另一个被阻止的任务,等待另一个被阻止的任务,等等。如果被阻止的任务链循环到第一个,没有人可以取得任何进展,你就会陷入僵局。 -如果在运行程序时没有立即出现死锁,则会出现最大的问题。您的系统可能容易出现死锁,并且只会在某些条件下死锁。程序可能在某个平台上运行正常,例如您的开发机器,但是当您将其部署到不同的硬件时会开始死锁。 +如果在运行程序时没有立即出现死锁,则会出现最大的问题。你的系统可能容易出现死锁,并且只会在某些条件下死锁。程序可能在某个平台上运行正常,例如你的开发机器,但是当你将其部署到不同的硬件时会开始死锁。 死锁通常源于细微的编程错误;一系列无辜的决定,最终意外地创建了一个依赖循环。本节包含一个经典示例,演示了死锁的特性。 @@ -279,7 +279,7 @@ Java 8 CompletableFuture是一个更好的解决方案:它允许您将操作 ## 并行流 -Java 8流的一个显着优点是,在某些情况下,它们可以很容易地并行化。这来自仔细的库设计,特别是流使用内部迭代的方式 - 也就是说,它们控制着自己的迭代器。特别是,他们使用一种特殊的迭代器,称为Spliterator,它被限制为易于自动分割。这产生了相当神奇的结果,即能够简单用parallel()然后流中的所有内容都作为一组并行任务运行。如果您的代码是使用Streams编写的,那么并行化以提高速度似乎是一种琐事 +Java 8流的一个显着优点是,在某些情况下,它们可以很容易地并行化。这来自仔细的库设计,特别是流使用内部迭代的方式 - 也就是说,它们控制着自己的迭代器。特别是,他们使用一种特殊的迭代器,称为Spliterator,它被限制为易于自动分割。这产生了相当神奇的结果,即能够简单用parallel()然后流中的所有内容都作为一组并行任务运行。如果你的代码是使用Streams编写的,那么并行化以提高速度似乎是一种琐事 例如,考虑来自Streams的Prime.java。查找质数可能是一个耗时的过程,我们可以看到该程序的计时: @@ -311,17 +311,20 @@ public class ParallelPrime { Files.write(Paths.get("primes.txt"), primes, StandardOpenOption.CREATE); } } - /* +``` + +输出结果: + +``` Output: 1224 - */ ``` 请注意,这不是微基准测试,因为我们计时整个程序。我们将数据保存在磁盘上以防止过激的优化;如果我们没有对结果做任何事情,那么一个高级的编译器可能会观察到程序没有意义并且消除了计算(这不太可能,但并非不可能)。请注意使用nio2库编写文件的简单性(在[文件](./17-Files.md)一章中有描述)。 当我注释掉[1] parallel()行时,我的结果大约是parallel()的三倍。 -并行流似乎是一个甜蜜的交易。您所需要做的就是将编程问题转换为流,然后插入parallel()以加快速度。实际上,有时候这很容易。但遗憾的是,有许多陷阱。 +并行流似乎是一个甜蜜的交易。你所需要做的就是将编程问题转换为流,然后插入parallel()以加快速度。实际上,有时候这很容易。但遗憾的是,有许多陷阱。 - parallel()不是灵丹妙药 @@ -367,11 +370,15 @@ public class Summing { // .limit(SZ+1).sum()); } } -/* Output:5000000050000000 +``` + +输出结果: + +``` +5000000050000000 Sum Stream: 167ms Sum Stream Parallel: 46ms Sum Iterated: 284ms -*/ ``` **CHECK**值是使用Carl Friedrich Gauss在1700年代后期仍在小学时创建的公式计算出来的. @@ -420,13 +427,17 @@ public class Summing2 { }); } } -/* Output:200000010000000 +``` + +输出结果: + +``` +200000010000000 Array Stream Sum: 104ms Parallel: 81ms Basic Sum: 106ms parallelPrefix: 265ms -*/ ``` 第一个限制是内存大小;因为数组是预先分配的,所以我们不能创建几乎与以前版本一样大的任何东西。并行化可以加快速度,甚至比使用 **basicSum()** 循环更快。有趣的是, **Arrays.parallelPrefix()** 似乎实际上减慢了速度。但是,这些技术中的任何一种在其他条件下都可能更有用 - 这就是为什么你不能做出任何确定性的声明,除了“你必须尝试一下”。” @@ -464,13 +475,17 @@ public class Summing3 { }); } } -/* Output:50000005000000 +``` + +输出结果: + +``` +50000005000000 Long Array Stream Reduce: 1038ms Long Basic Sum: 21ms Long parallelPrefix: 3616ms -*/ ``` 现在可用的内存量大约减半,并且所有情况下所需的时间都会很长,除了**basicSum()**,它只是循环遍历数组。令人惊讶的是, **Arrays.parallelPrefix()** 比任何其他方法都要花费更长的时间。 @@ -493,9 +508,13 @@ public class Summing4 { .reduce(0L,Long::sum)); } } -/* Output:50000005000000 +``` + +输出结果: + +``` +50000005000000 Long Parallel: 1014ms -*/ ``` 它比非parallel()版本略快,但并不显着。 @@ -524,7 +543,7 @@ Long Parallel: 1008ms** - parallel()/limit()交点 -使用parallel()时会有更复杂的问题。从其他语言中吸取的流是围绕无限流模型设计的。如果您拥有有限数量的元素,则可以使用集合以及为有限大小的集合设计的关联算法。如果您使用无限流,则使用针对流优化的算法。 +使用parallel()时会有更复杂的问题。从其他语言中吸取的流是围绕无限流模型设计的。如果你拥有有限数量的元素,则可以使用集合以及为有限大小的集合设计的关联算法。如果你使用无限流,则使用针对流优化的算法。 Java 8将两者合并起来。例如,**Collections**没有内置的**map()**操作。在Collection和Map中唯一类似流的批处理操作是**forEach()**。如果要执行**map()**和**reduce()**等操作,必须首先将Collection转换为存在这些操作的Stream: @@ -547,7 +566,11 @@ public class CollectionIntoStream { System.out.println(result); } } -/* Output:btpen +``` + +输出结果: + +``` pccux szgvg meinn @@ -558,7 +581,6 @@ ygpoa lkljl bynxt :PENCUXGVGINNLOZVEWPPCPOALJLNXT -*/ ``` **Collection**确实有一些批处理操作,如**removeAll()**,**removeIf()**和**retainAll()**,但这些都是破坏性的操作.**ConcurrentHashMap**对**forEachand**和**reduce**操作有特别广泛的支持。 @@ -628,15 +650,17 @@ public class ParallelStreamPuzzle2 { Files.write(Paths.get("PSP2.txt"), trace); } } -/* -Output: -[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -*/ ``` -current是使用线程安全的**AtomicInteger**类定义的,可以防止竞争条件;**parallel()**允许多个线程调用**get()**。 +输出结果: -在查看**PSP2.txt**。**IntGenerator.get()**被调用1024次时,您可能会感到惊讶。 +``` +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + +current是使用线程安全的 **AtomicInteger** 类定义的,可以防止竞争条件;**parallel()**允许多个线程调用**get()**。 + +在查看 **PSP2.txt**.**IntGenerator.get()** 被调用1024次时,你可能会感到惊讶。 **0: main 1: ForkJoinPool.commonPool-worker-1 @@ -685,7 +709,11 @@ public class ParallelStreamPuzzle3 { System.out.println(x); } } -/* Output: +``` + +输出结果: + +``` 8: main 6: ForkJoinPool.commonPool-worker-5 3: ForkJoinPool.commonPool-worker-7 @@ -697,12 +725,11 @@ public class ParallelStreamPuzzle3 { 7: ForkJoinPool.commonPool-worker-1 9: ForkJoinPool.commonPool-worker-2 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -*/ ``` 为了表明**parallel()**确实有效,我添加了一个对**peek()**的调用,这是一个主要用于调试的流函数:它从流中提取一个值并执行某些操作但不影响从流向下传递的元素。注意这会干扰线程行为,但我只是尝试在这里做一些事情,而不是实际调试任何东西。 -您还可以看到boxed()的添加,它接受int流并将其转换为Integer流。 +你还可以看到boxed()的添加,它接受int流并将其转换为Integer流。 现在我们得到多个线程产生不同的值,但它只产生10个请求的值,而不是1024个产生10个值。 @@ -710,20 +737,20 @@ public class ParallelStreamPuzzle3 { - 并行流只看起来很容易 -实际上,在许多情况下,并行流确实可以毫不费力地更快地产生结果。但正如您所见,只需将**parallel()**打到您的Stream操作上并不一定是安全的事情。在使用**parallel()**之前,您必须了解并行性如何帮助或损害您的操作。有个错误认识是认为并行性总是一个好主意。事实上并不是。Stream意味着您不需要重写所有代码以便并行运行它。流什么都不做的是取代理解并行性如何工作的需要,以及它是否有助于实现您的目标。 +实际上,在许多情况下,并行流确实可以毫不费力地更快地产生结果。但正如你所见,只需将**parallel()**打到你的Stream操作上并不一定是安全的事情。在使用**parallel()**之前,你必须了解并行性如何帮助或损害你的操作。有个错误认识是认为并行性总是一个好主意。事实上并不是。Stream意味着你不需要重写所有代码以便并行运行它。流什么都不做的是取代理解并行性如何工作的需要,以及它是否有助于实现你的目标。 ## 创建和运行任务 -如果无法通过并行流实现并发,则必须创建并运行自己的任务。稍后您将看到运行任务的理想Java 8方法是CompletableFuture,但我们将使用更基本的工具介绍概念。 +如果无法通过并行流实现并发,则必须创建并运行自己的任务。稍后你将看到运行任务的理想Java 8方法是CompletableFuture,但我们将使用更基本的工具介绍概念。 Java并发的历史始于非常原始和有问题的机制,并且充满了各种尝试的改进。这些主要归入附录:[低级并发(Appendix: Low-Level Concurrency)](./Appendix-Low-Level-Concurrency.md)。在这里,我们将展示一个规范形式,表示创建和运行任务的最简单,最好的方法。与并发中的所有内容一样,存在各种变体,但这些变体要么降级到该附录,要么超出本书的范围。 - Tasks and Executors -在Java的早期版本中,您通过直接创建自己的Thread对象来使用线程,甚至将它们子类化以创建您自己的特定“任务线程”对象。你手动调用了构造函数并自己启动了线程。 +在Java的早期版本中,你通过直接创建自己的Thread对象来使用线程,甚至将它们子类化以创建你自己的特定“任务线程”对象。你手动调用了构造函数并自己启动了线程。 -创建所有这些线程的开销变得非常重要,现在不鼓励采用实际操作方法。在Java 5中,添加了类来为您处理线程池。您可以将任务创建为单独的类型,然后将其交给ExecutorService以运行该任务,而不是为每种不同类型的任务创建新的Thread子类型。ExecutorService为您管理线程,并且在运行任务后重新循环线程而不是丢弃线程。 +创建所有这些线程的开销变得非常重要,现在不鼓励采用实际操作方法。在Java 5中,添加了类来为你处理线程池。你可以将任务创建为单独的类型,然后将其交给ExecutorService以运行该任务,而不是为每种不同类型的任务创建新的Thread子类型。ExecutorService为你管理线程,并且在运行任务后重新循环线程而不是丢弃线程。 首先,我们将创建一个几乎不执行任务的任务。它“sleep”(暂停执行)100毫秒,显示其标识符和正在执行任务的线程的名称,然后完成: @@ -770,7 +797,7 @@ public class Nap { ``` 为了消除异常处理的视觉噪声,这被定义为实用程序。第二个构造函数在超时时显示一条消息 -对**TimeUnit.MILLISECONDS.sleep()**的调用获取“当前线程”并在参数中将其置于休眠状态,这意味着该线程被挂起。这并不意味着底层处理器停止。操作系统将其切换到其他任务,例如在您的计算机上运行另一个窗口。OS任务管理器定期检查**sleep()**是否超时。当它执行时,线程被“唤醒”并给予更多处理时间。 +对**TimeUnit.MILLISECONDS.sleep()**的调用获取“当前线程”并在参数中将其置于休眠状态,这意味着该线程被挂起。这并不意味着底层处理器停止。操作系统将其切换到其他任务,例如在你的计算机上运行另一个窗口。OS任务管理器定期检查**sleep()**是否超时。当它执行时,线程被“唤醒”并给予更多处理时间。 你可以看到**sleep()**抛出一个已检查的**InterruptedException**;这是原始Java设计中的一个工件,它通过突然断开它们来终止任务。因为它往往会产生不稳定的状态,所以后来不鼓励终止。但是,我们必须在需要或仍然发生终止的情况下捕获异常。 @@ -798,7 +825,11 @@ public class SingleThreadExecutor { } } } -/* Output: +``` + +输出结果: + +``` All tasks submitted main awaiting termination main awaiting termination @@ -821,7 +852,6 @@ main awaiting termination NapTask[8] pool-1-thread-1 main awaiting termination NapTask[9] pool-1-thread-1 -*/ ``` 首先请注意,没有**SingleThreadExecutor**类。**newSingleThreadExecutor()**是**Executors**中的工厂,它创建特定类型的[^4] @@ -846,7 +876,11 @@ public class SingleThreadExecutor2 { exec.shutdown(); } } -/* Output: +``` + +输出结果: + +``` NapTask[0] pool-1-thread-1 NapTask[1] pool-1-thread-1 NapTask[2] pool-1-thread-1 @@ -857,7 +891,6 @@ NapTask[6] pool-1-thread-1 NapTask[7] pool-1-thread-1 NapTask[8] pool-1-thread-1 NapTask[9] pool-1-thread-1 -*/ ``` 一旦你callexec.shutdown(),尝试提交新任务将抛出RejectedExecutionException。 @@ -878,17 +911,19 @@ public class MoreTasksAfterShutdown { } } } -/* Output: -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 -*/ +``` +输出结果: + +``` +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 ``` **exec.shutdown()**的替代方法是**exec.shutdownNow()**,它除了不接受新任务外,还会尝试通过中断任务来停止任何当前正在运行的任务。同样,中断是错误的,容易出错并且不鼓励。 - 使用更多线程 -使用线程的重点是(几乎总是)更快地完成任务,那么我们为什么要限制自己使用SingleThreadExecutor呢?查看执行**Executors**的Javadoc,您将看到更多选项。例如CachedThreadPool: +使用线程的重点是(几乎总是)更快地完成任务,那么我们为什么要限制自己使用SingleThreadExecutor呢?查看执行**Executors**的Javadoc,你将看到更多选项。例如CachedThreadPool: ```java // concurrent/CachedThreadPool.java @@ -904,7 +939,11 @@ public class CachedThreadPool { exec.shutdown(); } } -/* Output: +``` + +输出结果: + +``` NapTask[7] pool-1-thread-8 NapTask[4] pool-1-thread-5 NapTask[1] pool-1-thread-2 @@ -915,8 +954,6 @@ NapTask[2] pool-1-thread-3 NapTask[9] pool-1-thread-10 NapTask[6] pool-1-thread-7 NapTask[5] pool-1-thread-6 -*/ - ``` 当你运行这个程序时,你会发现它完成得更快。这是有道理的,而不是使用相同的线程来顺序运行每个任务,每个任务都有自己的线程,所以它们都并行运行。似乎没有缺点,很难看出为什么有人会使用SingleThreadExecutor。 @@ -958,7 +995,11 @@ public class CachedThreadPool2 { exec.shutdown(); } } -/* Output: +``` + +输出结果: + +``` 0 pool-1-thread-1 200 1 pool-1-thread-2 200 4 pool-1-thread-5 300 @@ -969,8 +1010,6 @@ public class CachedThreadPool2 { 7 pool-1-thread-8 800 3 pool-1-thread-4 900 6 pool-1-thread-7 1000 -*/ - ``` 输出不是我们所期望的,并且从一次运行到下一次运行会有所不同。问题是所有的任务都试图写入val的单个实例,并且他们正在踩着彼此的脚趾。我们说这样的类不是线程安全的。让我们看看SingleThreadExecutor会发生什么: @@ -989,7 +1028,11 @@ public class SingleThreadExecutor3 { exec.shutdown(); } } -/* Output: +``` + +输出结果: + +``` 0 pool-1-thread-1 100 1 pool-1-thread-1 200 2 pool-1-thread-1 300 @@ -1000,8 +1043,6 @@ public class SingleThreadExecutor3 { 7 pool-1-thread-1 800 8 pool-1-thread-1 900 9 pool-1-thread-1 1000 -*/ - ``` 现在我们每次都得到一致的结果,尽管**InterferingTask**缺乏线程安全性。这是SingleThreadExecutor的主要好处 - 因为它一次运行一个任务,这些任务不会相互干扰,因此强加了线程安全性。这种现象称为线程限制,因为在单线程上运行任务限制了它们的影响。线程限制限制了加速,但可以节省很多困难的调试和重写。 @@ -1035,7 +1076,7 @@ public class CountingTask implements Callable { **call()完全独立于所有其他CountingTasks生成其结果**,这意味着没有可变的共享状态 -**ExecutorService**允许您使用**invokeAll()**启动集合中的每个Callable: +**ExecutorService**允许你使用**invokeAll()**启动集合中的每个Callable: ```java // concurrent/CachedThreadPool3.java @@ -1066,7 +1107,11 @@ public class CachedThreadPool3 { exec.shutdown(); } } -/* Output: +``` + +输出结果: + +``` 1 pool-1-thread-2 100 0 pool-1-thread-1 100 4 pool-1-thread-5 100 @@ -1078,11 +1123,10 @@ public class CachedThreadPool3 { 6 pool-1-thread-7 100 7 pool-1-thread-8 100 sum = 1000 -*/ ``` -只有在所有任务完成后,**invokeAll()**才会返回一个**Future**列表,每个任务一个**Future**。**Future**是Java 5中引入的机制,允许您提交任务而无需等待它完成。在这里,我们使用**ExecutorService.submit()**: +只有在所有任务完成后,**invokeAll()**才会返回一个**Future**列表,每个任务一个**Future**。**Future**是Java 5中引入的机制,允许你提交任务而无需等待它完成。在这里,我们使用**ExecutorService.submit()**: ```java // concurrent/Futures.java @@ -1099,10 +1143,13 @@ public class Futures { exec.shutdown(); } } -/* Output: +``` + +输出结果: + +``` 99 pool-1-thread-1 100 100 -*/ ``` - [1] 当你的任务尚未完成的**Future**上调用**get()**时,调用会阻塞(等待)直到结果可用。 @@ -1111,7 +1158,7 @@ public class Futures { 还要注意在**CachedThreadPool3.java.get()**中抛出异常,因此**extractResult()**在Stream中执行此提取。 -因为当你调用**get()**时,**Future**会阻塞,所以它只能解决等待任务完成的问题。最终,**Futures**被认为是一种无效的解决方案,现在不鼓励,支持Java 8的**CompletableFuture**,我们将在本章后面探讨。当然,您仍会在遗留库中遇到Futures +因为当你调用**get()**时,**Future**会阻塞,所以它只能解决等待任务完成的问题。最终,**Futures**被认为是一种无效的解决方案,现在不鼓励,支持Java 8的**CompletableFuture**,我们将在本章后面探讨。当然,你仍会在遗留库中遇到Futures 我们可以使用并行Stream以更简单,更优雅的方式解决这个问题: @@ -1131,7 +1178,11 @@ public class CountingStream { .reduce(0, Integer::sum)); } } -/* Output: +``` + +输出结果: + +``` 1 ForkJoinPool.commonPool-worker-3 100 8 ForkJoinPool.commonPool-worker-2 100 0 ForkJoinPool.commonPool-worker-6 100 @@ -1143,16 +1194,15 @@ public class CountingStream { 5 ForkJoinPool.commonPool-worker-2 100 3 ForkJoinPool.commonPool-worker-3 100 1000 -*/ ``` 这不仅更容易理解,我们需要做的就是将**parallel()**插入到其他顺序操作中,然后一切都在同时运行。 - Lambda和方法引用作为任务 -使用lambdas和方法引用,您不仅限于使用**Runnables**和**Callables**。因为Java 8通过匹配签名来支持lambda和方法引用(即,它支持结构一致性),所以我们可以将notRunnables或Callables的参数传递给ExecutorService: +使用lambdas和方法引用,你不仅限于使用**Runnables**和**Callables**。因为Java 8通过匹配签名来支持lambda和方法引用(即,它支持结构一致性),所以我们可以将notRunnables或Callables的参数传递给ExecutorService: -使用lambdas和方法引用,您不仅限于使用**Runnables**和**Callables**。因为Java 8通过匹配签名来支持lambda和方法引用(即,它支持结构一致性),所以我们可以将不是**Runnables**或**Callables**的参数传递给**ExecutorService**: +使用lambdas和方法引用,你不仅限于使用**Runnables**和**Callables**。因为Java 8通过匹配签名来支持lambda和方法引用(即,它支持结构一致性),所以我们可以将不是**Runnables**或**Callables**的参数传递给**ExecutorService**: ```java // concurrent/LambdasAndMethodReferences.java @@ -1182,23 +1232,26 @@ public class LambdasAndMethodReferences { exec.shutdown(); } } -/* Output: +``` + +输出结果: + +``` Lambda1 NotCallable NotRunnable Lambda2 -*/ ``` -这里,前两个**submit()**调用可以改为调用**execute()**。所有**submit()**调用都返回**Futures**,您可以在后两次调用的情况下提取结果。 +这里,前两个**submit()**调用可以改为调用**execute()**。所有**submit()**调用都返回**Futures**,你可以在后两次调用的情况下提取结果。 ## 终止耗时任务 -并发程序通常使用长时间运行的任务。可调用任务在完成时返回值;虽然这给它一个有限的寿命,但仍然可能很长。可运行的任务有时被设置为永远运行的后台进程。您经常需要一种方法在正常完成之前停止**Runnable**和**Callable**任务,例如当您关闭程序时。 +并发程序通常使用长时间运行的任务。可调用任务在完成时返回值;虽然这给它一个有限的寿命,但仍然可能很长。可运行的任务有时被设置为永远运行的后台进程。你经常需要一种方法在正常完成之前停止**Runnable**和**Callable**任务,例如当你关闭程序时。 -最初的Java设计提供了中断运行任务的机制(为了向后兼容,仍然存在);中断机制包括阻塞问题。中断任务既乱又复杂,因为您必须了解可能发生中断的所有可能状态,以及可能导致的数据丢失。使用中断被视为反对模式,但我们仍然被迫接受。 +最初的Java设计提供了中断运行任务的机制(为了向后兼容,仍然存在);中断机制包括阻塞问题。中断任务既乱又复杂,因为你必须了解可能发生中断的所有可能状态,以及可能导致的数据丢失。使用中断被视为反对模式,但我们仍然被迫接受。 InterruptedException,因为设计的向后兼容性残留。 @@ -1238,7 +1291,7 @@ public class QuittableTask implements Runnable { - [1]:只要运行标志为true,此任务的run()方法将继续。 - [2]: 显示仅在任务退出时发生。 -需要**running AtomicBoolean**证明编写Java program并发时最基本的困难之一是,如果**running**是一个普通的布尔值,你可能无法在执行程序中看到问题。实际上,在这个例子中,你可能永远不会有任何问题 - 但是代码仍然是不安全的。编写表明该问题的测试可能很困难或不可能。因此,您没有任何反馈来告诉您已经做错了。通常,您编写线程安全代码的唯一方法就是通过了解事情可能出错的所有细微之处。 +需要**running AtomicBoolean**证明编写Java program并发时最基本的困难之一是,如果**running**是一个普通的布尔值,你可能无法在执行程序中看到问题。实际上,在这个例子中,你可能永远不会有任何问题 - 但是代码仍然是不安全的。编写表明该问题的测试可能很困难或不可能。因此,你没有任何反馈来告诉你已经做错了。通常,你编写线程安全代码的唯一方法就是通过了解事情可能出错的所有细微之处。 作为测试,我们将启动很多QuittableTasks然后关闭它们。尝试使用较大的COUNT值 @@ -1262,9 +1315,13 @@ public class QuittingTasks { tasks.forEach(QuittableTask::quit); es.shutdown(); } } -/* Output: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 +``` + +输出结果: + +``` +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 -*/ ``` 我使用**peek()**将**QuittableTasks**传递给**ExecutorService**,然后将这些任务收集到**List.main()**中,只要任何任务仍在运行,就会阻止程序退出。即使为每个任务按顺序调用quit()方法,任务也不会按照它们创建的顺序关闭。独立运行的任务不会确定性地响应信号。 @@ -1295,7 +1352,12 @@ public class QuittingCompletable { cfutures.forEach(CompletableFuture::join); } } -/* Output: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*/ +``` + +输出结果: + +``` +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 **,就像在**QuittingTasks.java**中一样,但是在这个例子中,没有**peek()**将每个**QuittableTask**提交给**ExecutorService**。相反,在创建cfutures期间,每个任务都交给**CompletableFuture::runAsync**。这执行**VerifyTask.run(**)并返回**CompletableFuture **。因为**run()**不返回任何内容,所以在这种情况下我只使用**CompletableFuture**调用**join()**来等待它完成。 @@ -1386,18 +1448,20 @@ public class CompletableApply { cf4.thenApply(Machina::work); } } -/* Output: +``` + +输出结果: + +``` Machina0: ONE Machina0: TWO Machina0: THREE Machina0: complete -*/ - ``` **thenApply()**应用一个接受输入并产生输出的函数。在这种情况下,**work()**函数产生与它相同的类型,因此每个得到的**CompletableFuture**仍然被输入为**Machina**,但是(类似于**Streams**中的**map()**)**Function**也可以返回不同的类型,这将反映在返回类型 -您可以在此处看到有关**CompletableFutures**的重要信息:它们会在您执行操作时自动解包并重新包装它们所携带的对象。这样你就不会陷入麻烦的细节,这使得编写和理解代码变得更加简单。 +你可以在此处看到有关**CompletableFutures**的重要信息:它们会在你执行操作时自动解包并重新包装它们所携带的对象。这样你就不会陷入麻烦的细节,这使得编写和理解代码变得更加简单。 我们可以消除中间变量并将操作链接在一起,就像我们使用Streams一样: @@ -1417,18 +1481,20 @@ public class CompletableApplyChained { System.out.println(timer.duration()); } } -/* Output: +``` + +输出结果: + +``` Machina0: ONE Machina0: TWO Machina0: THREE Machina0: complete 514 -*/ - ``` 在这里,我们还添加了一个**Timer**,它向我们展示每一步增加100毫秒,还有一些额外的开销。 -**CompletableFutures**的一个重要好处是它们鼓励使用私有子类原则(不分享任何东西)。默认情况下,使用**thenApply()**来应用一个不与任何人通信的函数 - 它只需要一个参数并返回一个结果。这是函数式编程的基础,并且它在并发性方面非常有效[^5]。并行流和ComplempleFutures旨在支持这些原则。只要您不决定共享数据(共享非常容易,甚至意外)您可以编写相对安全的并发程序。 +**CompletableFutures**的一个重要好处是它们鼓励使用私有子类原则(不分享任何东西)。默认情况下,使用**thenApply()**来应用一个不与任何人通信的函数 - 它只需要一个参数并返回一个结果。这是函数式编程的基础,并且它在并发性方面非常有效[^5]。并行流和ComplempleFutures旨在支持这些原则。只要你不决定共享数据(共享非常容易,甚至意外)你可以编写相对安全的并发程序。 回调**thenApply()**开始一个操作,在这种情况下,在完成所有任务之前,不会完成**e CompletableFuture**的创建。虽然这有时很有用,但是启动所有任务通常更有价值,这样就可以运行时继续前进并执行其他操作。我们通过在操作结束时添加Async来实现此目的: @@ -1451,7 +1517,11 @@ public class CompletableApplyAsync { System.out.println(timer.duration()); } } -/* Output: +``` + +输出结果: + +``` 116 Machina0: ONE Machina0: TWO @@ -1459,19 +1529,18 @@ Machina0:THREE Machina0: complete Machina0: complete 552 -*/ ``` 同步调用(我们通常使用得那种)意味着“当你完成工作时,返回”,而异步调用以意味着“立刻返回但是继续后台工作。”正如你所看到的,**cf**的创建现在发生得跟快。每次调用 **thenApplyAsync()** 都会立刻返回,因此可以进行下一次调用,整个链接序列的完成速度比以前快得快。 事实上,如果没有回调**cf.join() t**方法,程序会在完成其工作之前退出(尝试取出该行)对**join()**阻止了**main()**进程的进行,直到cf操作完成,我们可以看到大部分时间的确在哪里度过。 -这种“立即返回”的异步能力需要**CompletableFuture**库进行一些秘密工作。特别是,它必须将您需要的操作链存储为一组回调。当第一个后台操作完成并返回时,第二个后台操作必须获取生成的**Machina**并开始工作,当完成后,下一个操作将接管,等等。但是没有我们普通的函数调用序列,通过程序调用栈控制,这个顺序会丢失,所以它使用回调 - 一个函数地址表来存储。 +这种“立即返回”的异步能力需要**CompletableFuture**库进行一些秘密工作。特别是,它必须将你需要的操作链存储为一组回调。当第一个后台操作完成并返回时,第二个后台操作必须获取生成的**Machina**并开始工作,当完成后,下一个操作将接管,等等。但是没有我们普通的函数调用序列,通过程序调用栈控制,这个顺序会丢失,所以它使用回调 - 一个函数地址表来存储。 -幸运的是,您需要了解有关回调的所有信息。程序员将你手工造成的混乱称为“回调地狱”。通过异步调用,**CompletableFuture**为您管理所有回调。除非你知道关于你的系统有什么特定的改变,否则你可能想要使用异步调用。 +幸运的是,你需要了解有关回调的所有信息。程序员将你手工造成的混乱称为“回调地狱”。通过异步调用,**CompletableFuture**为你管理所有回调。除非你知道关于你的系统有什么特定的改变,否则你可能想要使用异步调用。 - 其他操作 -当您查看**CompletableFuture**的Javadoc时,您会看到它有很多方法,但这个方法的大部分来自不同操作的变体。例如,有**thenApply()**,**thenApplyAsync()**和**thenApplyAsync()**的第二种形式,它接受运行任务的**Executor**(在本书中我们忽略了**Executor**选项)。 +当你查看**CompletableFuture**的Javadoc时,你会看到它有很多方法,但这个方法的大部分来自不同操作的变体。例如,有**thenApply()**,**thenApplyAsync()**和**thenApplyAsync()**的第二种形式,它接受运行任务的**Executor**(在本书中我们忽略了**Executor**选项)。 这是一个显示所有“基本”操作的示例,它们不涉及组合两个CompletableFutures或异常(我们将在稍后查看)。首先,我们将重复使用两个实用程序以提供简洁和方便: @@ -1549,7 +1618,11 @@ public class CompletableOperations { System.out.println("dependents: " + c.getNumberOfDependents()); } } -/* Output: +``` + +输出结果: + +``` 1 runAsync thenRunAsync @@ -1568,21 +1641,20 @@ java.util.concurrent.CompletableFuture@6d311334[Complet ed exceptionally] 777 dependents: 1 dependents: 2 -*/ ``` **main()**包含一系列可由其**int**值引用的测试。**cfi(1)**演示了**showr()**正常工作。**cfi(2)**是调用**runAsync()**的示例。由于**Runnable**不产生返回值,因此结果是**CompletableFuture **,因此使用**voidr()**。 注意使用**cfi(3)**,**thenRunAsync()**似乎与**runAsync()**一致,差异显示在后续的测试中: **runAsync()**是一个静态方法,所以你不会像**cfi(2)**一样调用它。相反你可以在**QuittingCompletable.java**中使用它。后续测试中**supplyAsync()**也是静态方法,但是需要一个**Supplier**而不是**Runnable**并产生一个**CompletableFuture**来代替**CompletableFuture**。 -含有“then”的方法将进一步的操作应用于现有的**CompletableFuture **。与**thenRunAsync()**不同的是,将**cfi(4)**,**cfi(5)**和**cfi(6)**的“ then”方法作为未包装的**Integer**的参数。如您通过使用**voidr()**所见,然后**AcceptAsync()**接受了一个**Consumer**,因此不会产生结果。**thenApplyAsync()**接受一个**Function**并因此产生一个结果(该结果的类型可以不同于其参数)。**thenComposeAsync()**与**thenApplyAsync()**非常相似,不同之处在于其Function必须产生已经包装在**CompletableFuture**中的结果。 +含有“then”的方法将进一步的操作应用于现有的**CompletableFuture **。与**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)**显示了如何通过给它一个结果来完成一个任务(**future**)(与**obtrudeValue()**相对,后者可能会迫使其结果替换该结果)。 如果你调用**CompletableFuture**中的**cancel()**方法,它也会完成并且是非常好的完成。 如果任务(**future**)未完成,则**getNow()**方法返回**CompletableFuture**的完成值,或者返回**getNow()**的替换参数。 -最后,我们看一下依赖(dependents)的概念。如果我们将两个**thenApplyAsync()**调用链接到**CompletableFuture**上,则依赖项的数量仍为1。但是,如果我们将另一个**thenApplyAsync()**直接附加到**c**,则现在有两个依赖项:两个链和另一个链。这表明您可以拥有一个**CompletionStage**,当它完成时,可以根据其结果派生多个新任务。 +最后,我们看一下依赖(dependents)的概念。如果我们将两个**thenApplyAsync()**调用链接到**CompletableFuture**上,则依赖项的数量仍为1。但是,如果我们将另一个**thenApplyAsync()**直接附加到**c**,则现在有两个依赖项:两个链和另一个链。这表明你可以拥有一个**CompletionStage**,当它完成时,可以根据其结果派生多个新任务。 ### 结合CompletableFutures -第二类**CompletableFuture**方法采用两个**CompletableFuture**并以各种方式将它们组合在一起。一个**CompletableFuture**通常会先于另一个完成,就好像两者都在比赛中一样。这些方法使您可以以不同的方式处理结果。 +第二类**CompletableFuture**方法采用两个**CompletableFuture**并以各种方式将它们组合在一起。一个**CompletableFuture**通常会先于另一个完成,就好像两者都在比赛中一样。这些方法使你可以以不同的方式处理结果。 为了对此进行测试,我们将创建一个任务,该任务将完成的时间作为其参数之一,因此我们可以控制。 **CompletableFuture**首先完成: @@ -1674,7 +1746,11 @@ public class DualCompletableOperations { join(); } } -/* Output: +``` + +输出结果: + +``` Workable[BW] runAfterEither Workable[AW] @@ -1713,14 +1789,13 @@ thenAcceptBoth: Workable[AW], Workable[BW] Workable[AW] ***************** allOf - */ ``` 为了便于访问,**cfA**和**cfB**是静态的。**init()**总是使用较短的延迟(因此总是“获胜”)使用“ B”初始化两者。**join()**是在这两种方法上调用**join()**并显示边框的另一种便捷方法。 所有这些“双重”方法都以一个**CompletableFuture**作为调用该方法的对象,第二个**CompletableFuture**作为第一个参数,然后是要执行的操作。 -通过使用**Shower()**和**void()**,您可以看到“运行”和“接受”是终端操作,而“应用”和“组合”产生了新的承载载荷的**CompletableFutures**。 +通过使用**Shower()**和**void()**,你可以看到“运行”和“接受”是终端操作,而“应用”和“组合”产生了新的承载载荷的**CompletableFutures**。 -方法的名称是不言自明的,您可以通过查看输出来验证这一点。一个特别有趣的方法是CombineAsync(),它等待两个**CompletableFuture**完成,然后将它们都交给BiFunction,然后BiFunction可以将结果加入到所得**CompletableFuture**的有效负载中。 +方法的名称是不言自明的,你可以通过查看输出来验证这一点。一个特别有趣的方法是CombineAsync(),它等待两个**CompletableFuture**完成,然后将它们都交给BiFunction,然后BiFunction可以将结果加入到所得**CompletableFuture**的有效负载中。 ### 模拟 @@ -1814,11 +1889,11 @@ public class FrostedCake { } ``` -一旦您对背后的想法感到满意。**CompletableFutures**它们相对易于使用。 +一旦你对背后的想法感到满意。**CompletableFutures**它们相对易于使用。 ### 例外情况 -与**CompletableFutur**e在处理链中包装对象的方式相同,它还可以缓冲异常。这些不会在处理过程中显示给调用者,而只会在您尝试提取结果时显示。为了展示它们是如何工作的,我们将从创建一个在某些情况下引发异常的类开始: +与**CompletableFutur**e在处理链中包装对象的方式相同,它还可以缓冲异常。这些不会在处理过程中显示给调用者,而只会在你尝试提取结果时显示。为了展示它们是如何工作的,我们将从创建一个在某些情况下引发异常的类开始: ```java // concurrent/Breakable.java @@ -1843,12 +1918,11 @@ public class Breakable { return b; } } - ``` -**failcount**为正时,每次将对象传递给**work()**方法可减少**failcount**。当它为零时,**work()**会引发异常。如果您给它的**failcount**为零,则它永远不会引发异常。 +**failcount**为正时,每次将对象传递给**work()**方法可减少**failcount**。当它为零时,**work()**会引发异常。如果你给它的**failcount**为零,则它永远不会引发异常。 请注意,它报告在抛出异常时抛出异常。 -在下面的**test()**方法中,**work()**多次应用于**Breakable**,因此,如果**failcount**在范围内,则会引发异常。但是,在测试**A**到**E**中,您可以从输出中看到抛出了异常,但是它们从未出现: +在下面的**test()**方法中,**work()**多次应用于**Breakable**,因此,如果**failcount**在范围内,则会引发异常。但是,在测试**A**到**E**中,你可以从输出中看到抛出了异常,但是它们从未出现: ```java // concurrent/CompletableExceptions.java @@ -1893,8 +1967,11 @@ public class CompletableExceptions { } } } +``` -/* Output: +输出结果: + +``` Throwing Exception for A Breakable_B [1] Throwing Exception for B @@ -1920,13 +1997,12 @@ Throwing Exception for H true done? false java.lang.RuntimeException: forced -*/ ``` 测试**A**到**E**运行到抛出异常的地步,然后……什么都没有。只有在测试**F**中调用**get()**时,我们才能看到抛出的异常。 -测试**G**显示,您可以首先检查在处理过程中是否引发了异常,而没有引发该异常。但是,测试H告诉我们,无论异常成功与否,异常仍然可以被视为“完成” +测试**G**显示,你可以首先检查在处理过程中是否引发了异常,而没有引发该异常。但是,测试H告诉我们,无论异常成功与否,异常仍然可以被视为“完成” 代码的最后一部分显示了如何在**CompletableFuture**中插入异常,而不管是否存在任何故障。 -加入或获取结果时,我们不会使用粗略的try-catch,而是使用**CompletableFuture**提供的更复杂的机制来自动响应异常。您可以使用与所有**CompletableFuture**相同的表格来执行此操作:在链中插入**CompletableFuture**调用。有三个选项:**exclusively(**),**handle()**和**whenComplete()**: +加入或获取结果时,我们不会使用粗略的try-catch,而是使用**CompletableFuture**提供的更复杂的机制来自动响应异常。你可以使用与所有**CompletableFuture**相同的表格来执行此操作:在链中插入**CompletableFuture**调用。有三个选项:**exclusively(**),**handle()**和**whenComplete()**: ```java // concurrent/CatchCompletableExceptions.java @@ -1973,7 +2049,11 @@ public class CatchCompletableExceptions { handleException(0); } } -/* Output: +``` + +输出结果: + +``` **** Failure Mode **** Breakable_exceptionally [1] Throwing Exception for exceptionally @@ -2002,12 +2082,11 @@ Breakable_whenComplete [-3] Breakable_whenComplete [-4] Breakable_whenComplete [-4] OK result: Breakable_whenComplete [-4] -*/ ``` 只有在有异常的情况下,**exclusively()**参数才会运行。**Exclusively()**的局限性在于,该函数只能返回输入的相同类型的值。**exclusively()**通过将一个好的对象重新插入流中而恢复到可行状态。 -**handle()**始终被调用,您必须检查一下**fail**是否为**true**才能查看是否发生了异常。但是**handle()**可以产生任何新类型,因此它使您可以执行处理,而不仅可以像**exception()**那样进行恢复。 -**whenComplete()**就像**handle()**一样,您必须测试是否失败,但是该参数是使用者,并且不会修改正在传递的结果对象。 +**handle()**始终被调用,你必须检查一下**fail**是否为**true**才能查看是否发生了异常。但是**handle()**可以产生任何新类型,因此它使你可以执行处理,而不仅可以像**exception()**那样进行恢复。 +**whenComplete()**就像**handle()**一样,你必须测试是否失败,但是该参数是使用者,并且不会修改正在传递的结果对象。 ### 流异常 @@ -2041,20 +2120,23 @@ public class StreamExceptions { } } } -/* Output: +``` + +输出结果: + +``` Entering try Breakable_C [2] Breakable_C [1] Throwing Exception for C Breakable_C failed -*/ ``` -使用**CompletableFutures**,我们看到了测试**A**到**E**的进展,但是使用**Streams**,直到您应用了终端操作(如[1]的**forEach()**),一切都没有开始。**CompletableFuture**执行工作并捕获任何异常以供以后检索。比较这两者并不是一件容易的事,因为**Stream**没有终端操作根本无法执行任何操作,但是**Stream**绝对不会存储其异常。 +使用**CompletableFutures**,我们看到了测试**A**到**E**的进展,但是使用**Streams**,直到你应用了终端操作(如[1]的**forEach()**),一切都没有开始。**CompletableFuture**执行工作并捕获任何异常以供以后检索。比较这两者并不是一件容易的事,因为**Stream**没有终端操作根本无法执行任何操作,但是**Stream**绝对不会存储其异常。 ### 检查异常 -CompletableFutures和并行Streams都不支持包含已检查异常的操作。相反,您必须在调用操作时处理检查到的异常,这会产生不太优雅的代码: +CompletableFutures和并行Streams都不支持包含已检查异常的操作。相反,你必须在调用操作时处理检查到的异常,这会产生不太优雅的代码: ```java // concurrent/ThrowsChecked.java @@ -2095,15 +2177,15 @@ public class ThrowsChecked { } ``` -如果您尝试像对 **nochecked()** 一样对 **withchecked()** 使用方法引用,则编译器会抱怨[1]和[2]。相反,您必须写出lambda表达式(或编写一个不会引发异常的包装器方法)。 +如果你尝试像对 **nochecked()** 一样对 **withchecked()** 使用方法引用,则编译器会抱怨[1]和[2]。相反,你必须写出lambda表达式(或编写一个不会引发异常的包装器方法)。 ## 死锁 -由于任务可能会被阻塞,因此一个任务有可能卡在等待另一个任务上,而任务又在等待另一个任务,依此类推,直到链回到第一个任务上。您会遇到一个不断循环的任务,彼此等待,没有人能动。这称为死锁[^6] -如果您尝试运行某个程序并立即陷入死锁,则可以立即查找该错误。真正的问题是,当您的程序看起来运行良好,但具有隐藏潜力死锁。在这里,您可能没有任何迹象表明可能发生死锁,因此该缺陷在您的程序中是潜在的,直到它意外发生为止(通常是对客户而言(几乎肯定很难复制))。因此,通过仔细的程序设计防止死锁是开发并发系统的关键部分。 +由于任务可能会被阻塞,因此一个任务有可能卡在等待另一个任务上,而任务又在等待另一个任务,依此类推,直到链回到第一个任务上。你会遇到一个不断循环的任务,彼此等待,没有人能动。这称为死锁[^6] +如果你尝试运行某个程序并立即陷入死锁,则可以立即查找该错误。真正的问题是,当你的程序看起来运行良好,但具有隐藏潜力死锁。在这里,你可能没有任何迹象表明可能发生死锁,因此该缺陷在你的程序中是潜在的,直到它意外发生为止(通常是对客户而言(几乎肯定很难复制))。因此,通过仔细的程序设计防止死锁是开发并发系统的关键部分。 埃德斯·迪克斯特拉(Essger Dijkstra)发明的"哲学家进餐"问题是经典的死锁例证。基本描述指定了五位哲学家(此处显示的示例允许任何数字)。这些哲学家将一部分时间花在思考上,一部分时间在吃饭上。他们在思考的时候并不需要任何共享资源,但是他们使用的餐具数量有限。在最初的问题描述中,器物是叉子,需要两个叉子才能从桌子中间的碗里取出意大利面。常见的版本是使用筷子。显然,每个哲学家都需要两个筷子才能吃饭。 引入了一个困难:作为哲学家,他们的钱很少,所以他们只能买五根筷子(更普遍地说,筷子的数量与哲学家相同)。它们之间围绕桌子隔开。当一个哲学家想要吃饭时,该哲学家必须拿起左边和右边的筷子。如果任一侧的哲学家都在使用所需的筷子,则我们的哲学家必须等待,直到必要的筷子可用为止。 -**StickHolder**类通过将单个筷子保持在大小为1的**BlockingQueue**中来管理它。**BlockingQueue**是一个设计用于在并发程序中安全使用的集合,如果您调用take()并且队列为空,则它将阻塞(等待)。将新元素放入队列后,将释放该块并返回该值: +**StickHolder**类通过将单个筷子保持在大小为1的**BlockingQueue**中来管理它。**BlockingQueue**是一个设计用于在并发程序中安全使用的集合,如果你调用take()并且队列为空,则它将阻塞(等待)。将新元素放入队列后,将释放该块并返回该值: ```java // concurrent/StickHolder.java @@ -2198,17 +2280,17 @@ public class DiningPhilosophers { } ``` -当您停止查看输出时,该程序将死锁。但是,根据您的计算机配置,您可能不会看到死锁。看来这取决于计算机上的内核数7。两个核心似乎不会产生死锁,但似乎有两个以上的核心很容易产生死锁。此行为使该示例更好地说明了死锁,因为您可能正在具有两个内核的计算机上编写程序(如果确实是导致问题的原因),并且确信该程序可以正常工作,只能启动它将其安装在另一台计算机上时出现死锁。请注意,仅仅因为您不容易看到死锁,并不意味着该程序就不会在两核计算机上死锁。该程序仍然容易死锁,很少发生-可以说是最坏的情况,因为问题不容易解决。 +当你停止查看输出时,该程序将死锁。但是,根据你的计算机配置,你可能不会看到死锁。看来这取决于计算机上的内核数7。两个核心似乎不会产生死锁,但似乎有两个以上的核心很容易产生死锁。此行为使该示例更好地说明了死锁,因为你可能正在具有两个内核的计算机上编写程序(如果确实是导致问题的原因),并且确信该程序可以正常工作,只能启动它将其安装在另一台计算机上时出现死锁。请注意,仅仅因为你不容易看到死锁,并不意味着该程序就不会在两核计算机上死锁。该程序仍然容易死锁,很少发生-可以说是最坏的情况,因为问题不容易解决。 在DiningPhilosophers构造函数中,每个哲学家都获得一个左右StickHolder的引用。除最后一个哲学家外,每个哲学家都通过以下方式初始化: 哲学家之间的下一双筷子。最后一位哲学家右手的筷子为零,因此圆桌会议完成了。那是因为最后一位哲学家正坐在第一个哲学家的旁边,而且他们俩都共用零筷子。[1]显示了以n为模数选择的右摇杆,将最后一个哲学家缠绕在第一个哲学家的旁边。 现在,所有哲学家都可以尝试吃饭,每个哲学家都在旁边等待哲学家放下筷子。 要开始在[3]上运行的每个Philosopher,我调用runAsync(),这意味着DiningPhilosophers构造函数立即在[4]处返回。没有任何东西可以阻止main()完成,该程序只是退出而无济于事。Nap对象阻止main()退出,然后在三秒钟后强制退出(可能是)死锁的程序。 -在给定的配置中,哲学家几乎没有时间思考。因此,他们都在尝试吃饭时争夺筷子,而且僵局往往很快发生。您可以更改此: +在给定的配置中,哲学家几乎没有时间思考。因此,他们都在尝试吃饭时争夺筷子,而且僵局往往很快发生。你可以更改此: 1. 通过增加[4]的值来添加更多哲学家。 2. 在Philosopher.java中取消注释行[1]。 -任一种方法都会减少死锁的可能性,这表明编写并发程序并认为它是安全的危险,因为它似乎“在我的机器上运行正常”。您可以轻松地说服自己该程序没有死锁,即使它不是。这个例子很有趣,因为它演示了程序似乎可以正确运行,同时仍然容易出现死锁。 +任一种方法都会减少死锁的可能性,这表明编写并发程序并认为它是安全的危险,因为它似乎“在我的机器上运行正常”。你可以轻松地说服自己该程序没有死锁,即使它不是。这个例子很有趣,因为它演示了程序似乎可以正确运行,同时仍然容易出现死锁。 为了解决该问题,我们观察到当四个同时满足条件: 1. 互斥。任务使用的至少一种资源必须不可共享。在这里,筷子一次只能由一位哲学家使用。 @@ -2216,19 +2298,20 @@ public class DiningPhilosophers { 3. 不能抢先从任务中夺走资源。任务仅作为正常事件释放资源。我们的哲学家很有礼貌,他们不会抓住其他哲学家的筷子。 4. 可能发生循环等待,即一个任务等待另一个任务持有的资源,而该任务又等待另一个任务持有的资源,依此类推,直到一个任务正在等待另一个任务持有的资源。第一项任务,从而使一切陷入僵局。在**DiningPhilosophers.java**中,发生循环等待是因为每个哲学家都先尝试获取右筷子,然后再获取左筷子。 -因为必须满足所有这些条件才能导致死锁,所以您只能阻止其中一个解除死锁。在此程序中,防止死锁的一种简单方法是打破第四个条件。之所以会发生这种情况,是因为每个哲学家都尝试按照特定的顺序拾起自己的筷子:先右后左。因此,每个哲学家都有可能在等待左手的同时握住右手的筷子,从而导致循环等待状态。但是,如果其中一位哲学家尝试首先拿起左筷子,则该哲学家决不会阻止紧邻右方的哲学家拿起筷子,从而排除了循环等待。 +因为必须满足所有这些条件才能导致死锁,所以你只能阻止其中一个解除死锁。在此程序中,防止死锁的一种简单方法是打破第四个条件。之所以会发生这种情况,是因为每个哲学家都尝试按照特定的顺序拾起自己的筷子:先右后左。因此,每个哲学家都有可能在等待左手的同时握住右手的筷子,从而导致循环等待状态。但是,如果其中一位哲学家尝试首先拿起左筷子,则该哲学家决不会阻止紧邻右方的哲学家拿起筷子,从而排除了循环等待。 在**DiningPhilosophers.java**中,取消注释[1]和其后的一行。这将原来的哲学家[1]替换为筷子颠倒的哲学家。通过确保第二位哲学家拾起并在右手之前放下左筷子,我们消除了死锁的可能性。 -这只是解决问题的一种方法。您也可以通过防止其他情况之一来解决它。 -没有语言支持可以帮助防止死锁;您有责任通过精心设计来避免这种情况。对于试图调试死锁程序的人来说,这些都不是安慰。当然,避免并发问题的最简单,最好的方法是永远不要共享资源-不幸的是,这并不总是可能的。 +这只是解决问题的一种方法。你也可以通过防止其他情况之一来解决它。 +没有语言支持可以帮助防止死锁;你有责任通过精心设计来避免这种情况。对于试图调试死锁程序的人来说,这些都不是安慰。当然,避免并发问题的最简单,最好的方法是永远不要共享资源-不幸的是,这并不总是可能的。 ## 构造函数非线程安全 -当你在脑子里想象一个对象构造的过程,你会很容易认为这个过程是线程安全的。毕竟,在对象初始化完成前没人能见到这个对象,所以又怎么会产生对于这个对象的争议呢?确实,java语言规范(JLS)自信满满地陈述道:“没有必要使构造器同步,因为它会锁定正在构造的对象,而这通常会使得该对象直到其所有构造器完成所有工作后,才对其他线程可见。” -不幸的是,对象构造过程像其他任何事物一样容易受到共享内存并发问题的影响,只是作用机制可能更微妙罢了。 -考虑使用静态字段为每个对象自动创建唯一标识符的过程。为了测试其不同的实现过程,我们从一个接口开始: +当你在脑子里想象一个对象构造的过程,你会很容易认为这个过程是线程安全的。毕竟,在对象初始化完成前对外不可见,所以又怎会对此产生争议呢?确实,Java 语言规范(JLS)自信满满地陈述道:“没必要使构造器的线程同步,因为它会锁定正在构造的对象,直到构造器完成初始化后才对其他线程可见。” +不幸的是,对象的构造过程如其他操作一样,也会受到共享内存并发问题的影响,只是作用机制可能更微妙罢了。 + +设想下使用一个静态字段为每个对象自动创建唯一标识符的过程。为了测试其不同的实现过程,我们从一个接口开始。代码示例: ```java //concurrent/HasID.java @@ -2237,7 +2320,7 @@ public interface HasID { } ``` -然后StaticIDField类以显式方式实现该接口: +然后 **StaticIDField** 类显式地实现该接口。代码示例: ```java // concurrent/StaticIDField.java @@ -2248,7 +2331,7 @@ public class StaticIDField implements HasID { } ``` -正如您所想的,此类是一个简单无害的类,它甚至没有一个显式的构造器来引发问题。当我们运行多个用于创建此类对象的线程时,究竟会发生什么,为了搞清楚这点,我们做了以下测试: +如你所想,该类是个简单无害的类,它甚至都没一个显式的构造器来引发问题。当我们运行多个用于创建此类对象的线程时,究竟会发生什么?为了搞清楚这点,我们做了以下测试。代码示例: ```java // concurrent/IDChecker.java @@ -2293,10 +2376,10 @@ public class IDChecker { } ``` -MakeObjects类是一个供应者类,包含一个能够产生List\类型的列表对象的get()方法。通过从每个HasID对象提取ID并放入列表中来生成这个列表对象,而test()方法则创建了两个并行的CompletableFuture对象,用于运行MakeObjects供应者类,然后获取运行结果。 -使用Guava库中的Sets.intersection()方法,计算出这两个返回的List\对象中有多少相同的ID(使用谷歌Guava库里的方法比使用官方的retainAll()方法速度快得多)。 +**MakeObjects** 类是一个生产者类,包含一个能够产生 List\ 类型的列表对象的 `get()` 方法。通过从每个 `HasID` 对象提取 `ID` 并放入列表中来生成这个列表对象,而 `test()` 方法则创建了两个并行的 **CompletableFuture** 对象,用于运行 **MakeObjects** 生产者类,然后获取运行结果。 +使用 Guava 库中的 **Sets.**`intersection()` 方法,计算出这两个返回的 List\ 对象中有多少相同的 `ID`(使用谷歌 Guava 库里的方法比使用官方的 `retainAll()` 方法速度快得多)。 -现在我们可以测试上面的StaticIDField类了: +现在我们可以测试上面的 **StaticIDField** 类了。代码示例: ```java // concurrent/TestStaticIDField.java @@ -2306,12 +2389,15 @@ public class TestStaticIDField { IDChecker.test(StaticIDField::new); } } -/* Output: - 13287 -*/ ``` -结果中出现了很多重复项。很显然,纯静态int用于构造过程并不是线程安全的。让我们使用AtomicInteger来使其变为线程安全的: +输出结果: + +``` + 13287 +``` + +结果中出现了很多重复项。很显然,纯静态 `int` 用于构造过程并不是线程安全的。让我们使用 **AtomicInteger** 来使其变为线程安全的。代码示例: ```java // concurrent/GuardedIDField.java @@ -2327,9 +2413,12 @@ public class GuardedIDField implements HasID { public static void main(String[] args) { IDChecker.test(GuardedIDField::new); } } -/* Output: +``` + +输出结果: + +``` 0 -*/ ``` 构造器有一种更微妙的状态共享方式:通过构造器参数: @@ -2379,14 +2468,17 @@ public class SharedConstructorArgument{ IDChecker.test(() -> new SharedUser(safe)); } } -/* Output: - 24838 - 0 -*/ ``` -在这里,SharedUser构造器实际上共享了相同的参数。即使SharedUser以完全无害且合理的方式使用其自己的参数,其构造器的调用方式也会引起冲突。SharedUser甚至不知道它是以这种方式调用的,更不必说控制它了。 -同步构造器并不被java语言所支持,但是通过使用同步语块来创建你自己的同步构造器是可能的(请参阅附录:Low-Level Concurrency,来进一步了解同步关键字——synchronized)。尽管JLS(java语言规范)这样陈述道:“……它会锁定正在构造的对象”,但这并不是真的——构造器实际上只是一个静态方法,因此同步构造器实际上会锁定该类的Class对象。我们可以通过创建自己的静态对象并锁定它,来达到与同步构造器相同的效果: +输出结果: + +``` + 24838 + 0 +``` + +在这里,**SharedUser** 构造器实际上共享了相同的参数。即使 **SharedUser** 以完全无害且合理的方式使用其自己的参数,其构造器的调用方式也会引起冲突。**SharedUser** 甚至不知道它是以这种方式调用的,更不必说控制它了。 +同步构造器并不被java语言所支持,但是通过使用同步语块来创建你自己的同步构造器是可能的(请参阅附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md),来进一步了解同步关键字—— `synchronized`)。尽管JLS(java语言规范)这样陈述道:“……它会锁定正在构造的对象”,但这并不是真的——构造器实际上只是一个静态方法,因此同步构造器实际上会锁定该类的Class对象。我们可以通过创建自己的静态对象并锁定它,来达到与同步构造器相同的效果: ```java // concurrent/SynchronizedConstructor.java @@ -2416,10 +2508,12 @@ public class SynchronizedConstructor{ IDChecker.test(() -> new SyncConstructor(unsafe)); } } -/* Output: - 0 -*/ +``` +输出结果: + +``` + 0 ``` Unsafe类的共享使用现在就变得安全了。另一种方法是将构造器设为私有(因此可以防止继承),并提供一个静态Factory方法来生成新对象: @@ -2451,17 +2545,20 @@ public class SynchronizedFactory{ IDChecker.test(() -> SyncFactory.factory(unsafe)); } } -/* Output: - 0 -*/ ``` -通过同步静态工厂方法,可以在构造过程中锁定Class对象。 -这些示例充分表明了在并发Java程序中检测和管理共享状态有多困难。即使您采取“不共享任何内容”的策略,也很容易产生意外的共享事件。 +输出结果: + +``` + 0 +``` + +通过同步静态工厂方法,可以在构造过程中锁定 **Class** 对象。 +这些示例充分表明了在并发Java程序中检测和管理共享状态有多困难。即使你采取“不共享任何内容”的策略,也很容易产生意外的共享事件。 ## 复杂性和代价 -假设您正在做披萨,我们把从整个流程的当前步骤到下一个步骤所需的工作量,在这里一一表示为枚举变量的一部分: +假设你正在做披萨,我们把从整个流程的当前步骤到下一个步骤所需的工作量,在这里一一表示为枚举变量的一部分: @@ -2562,7 +2659,11 @@ public class OnePizza{ })); } } -/* Output: +``` + +输出结果: + +``` Pizza 0: ROLLED Pizza 0: SAUCED Pizza 0: CHEESED @@ -2571,10 +2672,9 @@ Pizza 0: BAKED Pizza 0: SLICED Pizza 0: BOXED 1622 -*/ ``` -时间以毫秒为单位,加总所有步骤的工作量,会得出与我们的期望值相符的数字。 如果您以这种方式制作了五个披萨,那么您会认为它花费的时间是原来的五倍。 但是,如果这还不够快怎么办? 我们可以从尝试并行流方法开始: +时间以毫秒为单位,加总所有步骤的工作量,会得出与我们的期望值相符的数字。 如果你以这种方式制作了五个披萨,那么你会认为它花费的时间是原来的五倍。 但是,如果这还不够快怎么办? 我们可以从尝试并行流方法开始: ```java // concurrent/PizzaStreams.java @@ -2593,7 +2693,11 @@ public class PizzaStreams{ .forEach(za -> { while(!za.complete()) za.next(); }); System.out.println(timer.duration()); } } -/* Output: +``` + +输出结果: + +``` Pizza 2: ROLLED Pizza 0: ROLLED Pizza 1: ROLLED @@ -2630,12 +2734,11 @@ Pizza 0:BOXED Pizza 4:BOXED Pizza 3:BOXED 1739 -*/ ``` 现在,我们制作五个披萨的时间与制作单个披萨的时间就差不多了。 尝试删除标记为[1]的行后,你会发现它花费的时间是原来的五倍。 你还可以尝试将QUANTITY更改为4、8、10、16和17,看看会有什么不同,并猜猜看为什么会这样。 -PizzaStreams类产生的每个并行流在它的forEach()内完成所有工作,如果我们将其各个步骤用映射的方式一步一步处理,情况会有所不同吗? +**PizzaStreams** 类产生的每个并行流在它的forEach()内完成所有工作,如果我们将其各个步骤用映射的方式一步一步处理,情况会有所不同吗? ```java // concurrent/PizzaParallelSteps.java @@ -2663,7 +2766,11 @@ public class PizzaParallelSteps{ System.out.println(timer.duration()); } } -/* Output: +``` + +输出结果: + +``` Pizza 2: ROLLED Pizza 0: ROLLED Pizza 1: ROLLED @@ -2705,12 +2812,11 @@ Pizza 4: BOXED Pizza4: complete Pizza3: complete 1738 -*/ ``` -答案是“否”,事后看来这并不奇怪,因为每个披萨都需要按顺序执行步骤。因此,没法通过分步执行操作来进一步提高速度,就像上文的PizzaParallelSteps.java里面展示的一样。 +答案是“否”,事后看来这并不奇怪,因为每个披萨都需要按顺序执行步骤。因此,没法通过分步执行操作来进一步提高速度,就像上文的 `PizzaParallelSteps.java` 里面展示的一样。 -我们可以使用CompletableFutures重写这个例子: +我们可以使用 **CompletableFutures** 重写这个例子: ```java // concurrent/CompletablePizza.java @@ -2755,7 +2861,11 @@ public class CompletablePizza{ System.out.println(timer.duration()); } } -/* Output: +``` + +输出结果: + +``` 169 Pizza 0: ROLLED Pizza 1: ROLLED @@ -2798,18 +2908,17 @@ Pizza2: complete Pizza3: complete Pizza4: complete 1797 -*/ ``` -并行流和CompletableFutures是Java并发工具箱中最先进发达的技术。 您应该始终首先选择其中之一。 当一个问题很容易并行处理时,或者说,很容易把数据分解成相同的、易于处理的各个部分时,使用并行流方法处理最为合适(而如果您决定不借助它而由自己完成,您就必须撸起袖子,深入研究Spliterator的文档)。 +并行流和 **CompletableFutures** 是 Java 并发工具箱中最先进发达的技术。 你应该始终首先选择其中之一。 当一个问题很容易并行处理时,或者说,很容易把数据分解成相同的、易于处理的各个部分时,使用并行流方法处理最为合适(而如果你决定不借助它而由自己完成,你就必须撸起袖子,深入研究Spliterator的文档)。 -而当工作的各个部分内容各不相同时,使用CompletableFutures是最好的选择。比起面向数据,CompletableFutures更像是面向任务的。 +而当工作的各个部分内容各不相同时,使用 **CompletableFutures** 是最好的选择。比起面向数据,CompletableFutures** 更像是面向任务的。 对于披萨问题,结果似乎也没有什么不同。实际上,并行流方法看起来更简洁,仅出于这个原因,我认为并行流作为解决问题的首次尝试方法更具吸引力。 -由于制作披萨总需要一定的时间,无论您使用哪种并发方法,你能做到的最好情况,是在制作一个披萨的相同时间内制作n个披萨。 在这里当然很容易看出来,但是当您处理更复杂的问题时,您就可能忘记这一点。 通常,在项目开始时进行粗略的计算,就能很快弄清楚最大可能的并行吞吐量,这可以防止您因为采取无用的加快运行速度的举措而忙得团团转。 +由于制作披萨总需要一定的时间,无论你使用哪种并发方法,你能做到的最好情况,是在制作一个披萨的相同时间内制作n个披萨。 在这里当然很容易看出来,但是当你处理更复杂的问题时,你就可能忘记这一点。 通常,在项目开始时进行粗略的计算,就能很快弄清楚最大可能的并行吞吐量,这可以防止你因为采取无用的加快运行速度的举措而忙得团团转。 -使用CompletableFutures或许可以轻易地带来重大收益,但是在尝试更进一步时需要倍加小心,因为额外增加的成本和工作量会非常容易远远超出你之前拼命挤出的那一点点收益。 +使用 **CompletableFutures** 或许可以轻易地带来重大收益,但是在尝试更进一步时需要倍加小心,因为额外增加的成本和工作量会非常容易远远超出你之前拼命挤出的那一点点收益。 @@ -2820,7 +2929,7 @@ Pizza4: complete [^3]:有人谈论在Java——10中围绕泛型做一些类似的基本改进,这将是非常令人难以置信的。 [^4]:这是一种有趣的,虽然不一致的方法。通常,我们期望在公共接口上使用显式类表示不同的行为 [^5]:不,永远不会有纯粹的功能性Java。我们所能期望的最好的是一种在JVM上运行的全新语言。 -[^6]:当两个任务能够更改其状态以使它们不会被阻止但它们从未取得任何有用的进展时,您也可以使用活动锁。 +[^6]:当两个任务能够更改其状态以使它们不会被阻止但它们从未取得任何有用的进展时,你也可以使用活动锁。