From 1d5370c52dc2fdd8ae44770834736dd8b503ca5c Mon Sep 17 00:00:00 2001 From: TENCHIANG Date: Fri, 25 Sep 2020 16:48:52 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E7=BF=BB=E8=AF=91=E4=BC=98=E5=8C=96=2024-C?= =?UTF-8?q?oncurrent-Programming.md#=E5=B9=B6=E5=8F=91=E4=B8=BA=E9=80=9F?= =?UTF-8?q?=E5=BA=A6=E8=80=8C=E7=94=9F=20#Java=20=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E7=9A=84=E5=9B=9B=E5=8F=A5=E6=A0=BC=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/book/24-Concurrent-Programming.md | 78 +++++++++++++------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md index f0eaa0d2..58dbd92d 100755 --- a/docs/book/24-Concurrent-Programming.md +++ b/docs/book/24-Concurrent-Programming.md @@ -135,91 +135,91 @@ slowdown occurs): 在听说并发编程的问题之后,你可能会想知道它是否值得这么麻烦。答案是“不,除非你的程序运行速度不够快。”并且在决定用它之前你会想要仔细思考。不要随便跳进并发编程的悲痛之中。如果有一种方法可以在更快的机器上运行你的程序,或者如果你可以对其进行分析并发现瓶颈并在该位置替换更快的算法,那么请执行此操作。只有在显然没有其他选择时才开始使用并发,然后仅在必要的地方去使用它。 -速度问题一开始听起来很简单:如果你想要一个程序运行得更快,将其分解成碎片并在一个单独的处理器上运行每个部分。由于我们能够提高时钟速度流(至少对于传统芯片),速度的提高是出现在多核处理器的形式而不是更快的芯片。为了使你的程序运行得更快,你必须学会如何利用那些额外的处理器,这是并发性给你的一个建议。 +速度问题一开始听起来很简单:如果你想要一个程序运行得更快,将其分解为多个部分,并在单独的处理器上运行每个部分。随着我们提高时钟速度的能力耗尽(至少对传统芯片而言),速度的提高是出现在多核处理器的形式而不是更快的芯片。为了使程序运行得更快,您必须学会利用那些额外的处理器(译者注:处理器一般代表 CPU 的一个逻辑核心),这是并发所带来的好处之一。 -使用多处理器机器,可以在这些处理器之间分配多个任务,这可以显着提高吞吐量。强大的多处理器 Web 服务器通常就是这种情况,它可以在程序中为 CPU 分配大量用户请求,每个请求分配一个线程。 +对于多处理器机器,可以在这些处理器之间分配多个任务,这可以显著提高吞吐量。强大的多处理器 Web 服务器通常就是这种情况,它可以在程序中为 CPU 分配大量用户请求,每个请求分配一个线程。 -但是,并发性通常可以提高在单个处理器上运行的程序的性能。这听起来有点违反直觉。如果考虑一下,由于上下文切换的成本增加(从一个任务更改为另一个任务),在单个处理器上运行的并发程序实际上应该比程序的所有部分顺序运行具有更多的开销。在表面上,将程序的所有部分作为单个任务运行并节省上下文切换的成本似乎更便宜。 +但是,并发性通常可以提高在单个处理器上运行的程序的性能。这听起来有点违反直觉。你会这么想,由于上下文切换的成本增加(从一个任务切换到另一个任务),在单个处理器上运行的并发程序实际上应该比程序的所有部分顺序运行具有更多的开销。表面上看,将程序的所有部分作为单个任务运行并节省上下文切换的成本似乎更低。 -可以产生影响的问题是阻塞。如果你的程序中的一个任务由于程序控制之外的某些条件(通常是 I/O)而无法继续,我们会说任务或线程阻塞(在我们的科幻故事中,克隆体已敲门而且是等待它打开)。如果没有并发性,整个程序就会停止,直到外部条件发生变化。但是,如果使用并发编写程序,则当一个任务被阻止时,程序中的其他任务可以继续执行,因此程序继续向前移动。实际上,从性能的角度来看,在单处理器机器上使用并发是没有意义的,除非其中一个任务可能阻塞。 +使这个问题变得有些不同的是阻塞。如果程序中的某个任务由于程序控制之外的某种情况而无法继续(通常是 I/O),我们就称该任务或线程已阻塞(在我们的科幻故事中,就是克隆人已经敲门并等待它打开)。如果没有并发,整个程序就会停下来,直到外部条件发生变化。但是,如果使用并发编写程序,则当一个任务被阻塞时,程序中的其他任务可以继续执行,因此整个程序得以继续运行。事实上,从性能的角度来看,如果没有任务会阻塞,那么在单处理器机器上使用并发是没有意义的。 -单处理器系统中性能改进的一个常见例子是事件驱动编程,特别是用户界面编程。考虑一个程序执行一些长时间运行操作,从而最终忽略用户输入和无响应。如果你有一个“退出”按钮,你不想在你编写的每段代码中轮询它。这会产生笨拙的代码,无法保证程序员不会忘记执行检查。没有并发性,生成响应式用户界面的唯一方法是让所有任务定期检查用户输入。通过创建单独的执行线程来响应用户输入,该程序保证了一定程度的响应。 +单处理器系统中性能改进的一个常见例子是事件驱动编程,特别是用户界面编程。考虑一个程序执行一些耗时操作,最终忽略用户输入导致无响应。如果你有一个“退出”按钮,你不想在你编写的每段代码中都检查它的状态(轮询)。这会产生笨拙的代码,也无法保证程序员不会忘了检查。没有并发,生成可响应用户界面的唯一方法是让所有任务都定期检查用户输入。通过创建单独的线程以执行用户输入的响应,能够让程序保证一定程度的响应能力。 -实现并发的直接方法是在操作系统级别,使用与线程不同的进程。进程是一个在自己的地址空间内运行的自包含程序。进程很有吸引力,因为操作系统通常将一个进程与另一个进程隔离,因此它们不会相互干扰,这使得进程编程相对容易。相比之下,线程共享内存和 I/O 等资源,因此编写多线程程序时遇到的困难是在不同的线程驱动的任务之间协调这些资源,一次不能通过多个任务访问它们。 +实现并发的一种简单方式是使用操作系统级别的进程。与线程不同,进程是在其自己的地址空间中运行的独立程序。进程的优势在于,因为操作系统通常将一个进程与另一个进程隔离,因此它们不会相互干扰,这使得进程编程相对容易。相比之下,线程之间会共享内存和 I/O 等资源,因此编写多线程程序最基本的困难在于协调不同线程驱动的任务之间对这些资源的使用,以免这些资源不会同时被多个资源访问。 -有些人甚至提倡将进程作为并发的唯一合理方法[^1],但不幸的是,通常存在数量和开销限制,从而阻止了在并发范围内的适用性(最终你会习惯标准的并发限制,“这种方法适用于一些情况但不适用于其他情况”) +有些人甚至提倡将进程作为唯一合理的并发实现方式[^1],但遗憾的是,通常存在数量和开销方面的限制,从而阻止了进程在并发范围内的适用性(最终你会习惯标准的并发限制,“这种方法适用于一些情况但不适用于其他情况”) -一些编程语言旨在将并发任务彼此隔离。这些通常被称为_函数式语言_,其中每个函数调用不产生其他影响(因此不能与其他函数干涉),因此可以作为独立的任务来驱动。Erlang 就是这样一种语言,它包括一个任务与另一个任务进行通信的安全机制。如果你发现程序的某一部分必须大量使用并发性并且你在尝试构建该部分时遇到了过多的问题,那么你可能会考虑使用专用并发语言创建程序的那一部分。 +一些编程语言被设计为将并发任务彼此隔离。这些通常被称为_函数式语言_,其中每个函数调用不产生副作用(不会干扰到其它函数),所以可以作为独立的任务来驱动。Erlang 就是这样一种语言,它包括一个任务与另一个任务进行通信的安全机制。如果发现程序的某一部分必须大量使用并发,并且在尝试构建该部分时遇到了过多的问题,那么可以考虑使用这些专用的并发语言创建程序的这个部分。 -Java 采用了更传统的方法[^2],即在顺序语言之上添加对线程的支持而不是在多任务操作系统中分配外部进程,线程在执行程序所代表的单个进程中创建任务交换。 +Java 采用了更传统的方法[^2],即在顺序语言之上添加对线程的支持而不是在多任务操作系统中分叉外部进程,线程是在表示执行程序的单个进程内创建任务。 -并发性会带来成本,包括复杂性成本,但可以通过程序设计,资源平衡和用户便利性的改进来抵消。通常,并发性使你能够创建更加松散耦合的设计;除此以外,你必须特别关注那些使用了并发操作的代码。 +并发会带来各种成本,包括复杂性成本,但可以被程序设计,资源平衡和用户便利性方面的改进所抵消。通常,并发性使你能够创建更低耦合的设计;另一方面,你必须特别关注那些使用了并发操作的代码。 -## 四句格言 +## Java 并发的四句格言 -在经历了多年的 Java 并发之后,我总结了以下四个格言: ->1.不要这样做 +在经历了多年 Java 并发的实践之后,我总结了以下四个格言: +>1.不要用它(避免使用并发) > >2.没有什么是真的,一切可能都有问题 > ->3.它起作用,并不意味着它没有问题 +>3.仅仅是它能运行,并不意味着它没有问题 > ->4.你仍然必须理解它 +>4.你必须理解它(逃不掉并发) 这些格言专门针对 Java 的并发设计问题,尽管它们也可以适用于其他一些语言。但是,确实存在旨在防止这些问题的语言。 -### 1.不要这样做 +### 1.不要用它 -(不要自己动手) +(而且不要自己去实现它) -避免纠缠于并发产生的深层问题的最简单方法就是不要这样做。虽然它是诱人的,并且在做一些简单的事情时似乎足够安全,但它存在无数、微妙的陷阱。如果你可以避免它,你的生活会更容易。 +避免陷入并发所带来的玄奥问题的最简单方法就是不要用它。尽管尝试一些简单的东西可能很诱人,也似乎足够安全,但是陷阱却是无穷且微妙的。如果你能避免使用它,你的生活将会轻松得多。 -证明并发性的唯一因素是速度。如果你的程序运行速度不够快 - 在这里要小心,因为只是希望它运行得更快是不合理的 - 应该首先用一个分析器(参见代码校验章中分析和优化)来发现你是否可以执行其他一些优化。 +使用并发唯一的正当理由是速度。如果你的程序运行速度不够快——这里要小心,因为仅仅想让它运行得更快不是正当理由——应该首先用一个分析器(参见代码校验章中分析和优化)来发现你是否可以执行其他一些优化。 -如果你被迫进行并发,请采取最简单,最安全的方法来解决问题。使用众所周知的库并尽可能少地编写自己的代码。有了并发性,就没有“太简单了”。自负才是你的敌人。 +如果你被迫使用并发,请采取最简单,最安全的方法来解决问题。使用知名的库并尽可能少地自己编写代码。对于并发,就没有“太简单了”——自作聪明是你的敌人。 ### 2.没有什么是真的,一切可能都有问题 -没有并发性的编程,你会发现你的世界有一定的顺序和一致性。通过简单地将变量赋值给某个值,很明显它应该始终正常工作。 +不使用并发编程,你已经料到了你的世界具有确定的顺序和一致性。对于变量赋值这样简单的操作,很明显它应该总是能够正常工作。 -在并发领域,有些事情可能是真的而有些事情却不是,你必须认为没有什么是真的。你必须质疑一切。即使将变量设置为某个值也可能或者可能不会按预期的方式工作,并且从那里开始走下坡路。对于在并发中遇到那些看起来有效但实际上无效的东西,我已经很习惯了。 +在并发领域,有些事情可能是真的而有些事情却不是,以至于你必须假设没有什么是真的。你必须质疑一切。即使将变量设置为某个值也可能不会按预期的方式工作,事情从这里开始迅速恶化。我已经熟悉了这样一种感觉:我认为应该明显奏效的东西,实际上却行不通。 -在非并发程序中你可以忽略的各种事情在并发程序中突然变得非常重要。例如,你必须知道处理器缓存以及保持本地缓存与主内存一致的问题。你必须深入了解对象构造的复杂性,以便你的构造器不会意外地将数据暴露给其他线程进行更改。问题还有很多。 +所有在非并发编程中可以忽略的事情突然之间都因为并发而变得重要起来。例如,你必须了解处理器缓存以及保持本地缓存与主内存一致等问题。您必须理解对象构造的深层复杂性,以便构造函数不会意外地将数据暴露给其他线程更改。诸如此类问题不胜枚举。 -因为这些主题太复杂,本章无法为你提供更专业的知识(再次参见 Java Concurrency in Practice),但你必须了解它们。 +因为这些主题太复杂,以至于本章无法为你提供更专业的知识(再次参见 Java Concurrency in Practice),但你必须意识到它们。 -### 3.它起作用,并不意味着它没有问题 +### 3.仅仅是它能运行,并不意味着它没有问题 -我们很容易编写出一个看似完美实则有问题的并发程序,并且往往问题直在极端情况下才暴露出来 - 在程序部署后不可避免地会出现用户问题。 +我们很容易编写出一个看似正常实则有问题的并发程序,而且问题只有在极少的情况下才会显现出来——在程序部署后不可避免地会成为用户问题(投诉)。 -- 你不能证明并发程序是正确的,你只能(有时)证明它是不正确的。 -- 大多数情况下你甚至不能这样做:如果它有问题,你可能无法检测到它。 -- 你通常不能编写有用的测试,因此你必须依靠代码检查结合深入的并发知识来发现错误。 +- 你不能验证出并发程序是正确的,你只能(有时)验证出它是不正确的。 +- 大多数情况下你甚至没办法验证:如果它出问题了,你可能无法检测到它。 +- 你通常无法编写有用的测试,因此你必须依靠代码检查和对并发的深入了解来发现错误。 - 即使是有效的程序也只能在其设计参数下工作。当超出这些设计参数时,大多数并发程序会以某种方式失败。 -在其他 Java 主题中,我们培养了一种感觉-决定论。一切都按照语言的承诺(或隐含)进行,这是令人欣慰和期待的 - 毕竟,编程语言的目的是让机器做我们想要的。从确定性编程的世界进入并发编程领域,我们遇到了一种称为[Dunning-Kruger](https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect) 效应的认知偏差,可以概括为“无知者无畏。”这意味着“......相对不熟练的人拥有着虚幻的优越感,错误地评估他们的能力远高于实际。 +在其他 Java 主题中,我们养成了决定论的观念。一切都按照语言的承诺的(或暗示的)发生,这是令人欣慰的也是人们所期待的——毕竟,编程语言的意义就是让机器做我们想要它做的事情。从确定性编程的世界进入并发编程领域,我们遇到了一种称为 [邓宁-克鲁格效应](https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect) 的认知偏差,可以概括为“无知者无畏”,意思是:“相对不熟练的人拥有着虚幻的优越感,错误地评估他们的能力远高于实际。 -我自己的经验是,无论你是多么确定你的代码是线程安全的,它可能已经无效了。你可以很容易地了解所有的问题,然后几个月或几年后你会发现一些概念让你意识到你编写的大多数内容实际上都容易受到并发错误的影响。当某些内容不正确时,编译器不会告诉你。为了使它正确,你必须在研究代码前了解所有并发问题。 +我自己的经验是,无论你是多么确定你的代码是_线程安全_的,它都可能是有问题的。你可以很容易地了解所有的问题,然后几个月或几年后你会发现一些概念,让你意识到你编写的大多数代码实际上都容易受到并发 bug 的影响。当某些代码不正确时,编译器不会告诉你。为了使它正确,在研究代码时,必须将并发性的所有问题都放在前脑中。 -在 Java 的所有非并发领域,“没有明显的错误和没有明显的编译错误”似乎意味着一切都好。对于并发,它没有任何意义。在这种情况你最糟糕的表现就是“自信”。 +在 Java 的所有非并发领域,“没有明显的 bug 而且没有编译报错“似乎意味着一切都好。但对于并发,它没有任何意义。在这种情况你最糟糕的表现就是“自信”。 -### 4.你必须仍然理解 +### 4.你必须理解它 在格言 1-3 之后,你可能会对并发性感到害怕,并且认为,“到目前为止,我已经避免了它,也许我可以继续避免它。 -这是一种理性的反应。你可能知道其他编程语言更好地设计用于构建并发程序 - 甚至是在 JVM 上运行的程序(从而提供与 Java 的轻松通信),例如 Clojure 或 Scala。为什么不用这些语言编写并发部分并将 Java 用于其他所有部分呢? +这是一种理性的反应。你可能知道其他更好地被设计用于构建并发程序的编程语言——甚至是在 JVM 上运行的语言(从而提供与 Java 的轻松通信),例如 Clojure 或 Scala。为什么不用这些语言来编写并发部分,然后用Java来做其他的事情呢? 唉,你不能轻易逃脱: -- 即使你从未明确地创建一个线程,你可能使用的框架 - 例如,Swing 图形用户界面(GUI)库,或者像 **Timer** class 那样简单的东西。 -- 这是最糟糕的事情:当你创建组件时,你必须假设这些组件可能在多线程环境中重用。即使你的解决方案是放弃并声明你的组件“不是线程安全的”,你仍然必须知道这样的声明是重要的,它是什么意思? +- 即使你从未显示地创建一个线程,你使用的框架也可能——例如,Swing 图形用户界面(GUI)库,或者像 **Timer** 类(计时器)那样简单的东西。 +- 最糟糕的是:当你创建组件时,必须假设这些组件可能会在多线程环境中重用。即使你的解决方案是放弃并声明你的组件是“非线程安全的”,你仍然必须充分了解这样一个语句的重要性及其含义。 -人们有时会认为并发性太难,不能包含在介绍该语言的书中。他们认为并发是一个可以独立对待的独立主题,并且它在日常编程中出现的少数情况(例如图形用户界面)可以用特殊的习语来处理。如果你可以避免它,为什么要介绍这样的复杂的主题。 +人们有时会认为并发对于介绍语言的书来说太高级了,因此不适合放在其中。他们认为并发是一个独立的主题,并且对于少数出现在日常的程序设计中的情况(例如图形用户界面),可以用特殊的惯用法来处理。如果你可以回避,为什么还要介绍这么复杂的主题呢? -唉,如果只是这样的话,那就太好了。但不幸的是,你无法选择何时在 Java 程序中出现线程。仅仅你从未写过自己的线程,并不意味着你可以避免编写线程代码。例如,Web 系统是最常见的 Java 应用程序之一,本质上是多线程的 Web 服务器通常包含多个处理器,而并行性是利用这些处理器的理想方式。就像这样的系统看起来那么简单,你必须理解并发才能正确地编写它。 +唉,如果是这样就好了。遗憾的是,对于线程何时出现在 Java 程序中,这不是你能决定的。仅仅是你自己没有启动线程,并不代表你就可以回避编写使用线程的代码。例如,Web 系统是最常见的 Java 应用之一,本质上是多线程的 Web 服务器,通常包含多个处理器,而并行是利用这些处理器的理想方式。尽管这样的系统看起来很简单,但你必须理解并发才能正确地编写它。 -Java 是一种多线程语言,不管你有没有意识到并发问题,它就在那里。因此,有许多 Java 程序正在使用中,或者只是偶然工作,或者大部分时间工作并且不时地发生问题,因为。有时这种问题是相对良性的,但有时它意味着丢失有价值的数据,如果你没有意识到并发问题,你最终可能会把问题放在其他地方而不是你的代码中。如果将程序移动到多处理器系统,则可以暴露或放大这些类型的问题。基本上,了解并发性使你意识到正确的程序可能会表现出错误的行为。 +Java 是一种多线程语言,不管你有没有意识到并发问题,它就在那里。因此,有很多使用并发的 Java 程序,要么只是偶然运行,要么大部分时间都在运行,并且会因为未被发现的并发缺陷而时不时地神秘崩溃。有时这种崩溃是相对温和的,但有时它意味着丢失有价值的数据,如果你没有意识到并发问题,你最终可能会把问题归咎于其他地方而不是你的代码中。如果将程序移动到多处理器系统中,这些类型的问题还会被暴露或放大。基本上,了解并发可以使你意识到明显正确的程序也可能会表现出错误的行为。 ## 残酷的真相 From 44a40794f2c757874db8fda1e610d6ca4f3acc1a Mon Sep 17 00:00:00 2001 From: TENCHIANG Date: Tue, 29 Sep 2020 20:46:16 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E7=BF=BB=E8=AF=91=E4=BC=98=E5=8C=96=2024-C?= =?UTF-8?q?oncurrent-Programming.md#=E5=B9=B6=E5=8F=91=E4=B8=BA=E9=80=9F?= =?UTF-8?q?=E5=BA=A6=E8=80=8C=E7=94=9F=20#Java=20=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E7=9A=84=E5=9B=9B=E5=8F=A5=E6=A0=BC=E8=A8=80=20fix=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/book/24-Concurrent-Programming.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md index 58dbd92d..e8ed661e 100755 --- a/docs/book/24-Concurrent-Programming.md +++ b/docs/book/24-Concurrent-Programming.md @@ -135,25 +135,25 @@ slowdown occurs): 在听说并发编程的问题之后,你可能会想知道它是否值得这么麻烦。答案是“不,除非你的程序运行速度不够快。”并且在决定用它之前你会想要仔细思考。不要随便跳进并发编程的悲痛之中。如果有一种方法可以在更快的机器上运行你的程序,或者如果你可以对其进行分析并发现瓶颈并在该位置替换更快的算法,那么请执行此操作。只有在显然没有其他选择时才开始使用并发,然后仅在必要的地方去使用它。 -速度问题一开始听起来很简单:如果你想要一个程序运行得更快,将其分解为多个部分,并在单独的处理器上运行每个部分。随着我们提高时钟速度的能力耗尽(至少对传统芯片而言),速度的提高是出现在多核处理器的形式而不是更快的芯片。为了使程序运行得更快,您必须学会利用那些额外的处理器(译者注:处理器一般代表 CPU 的一个逻辑核心),这是并发所带来的好处之一。 +速度问题一开始听起来很简单:如果你想要一个程序运行得更快,将其分解为多个部分,并在单独的处理器上运行每个部分。随着我们提高时钟速度的能力耗尽(至少对传统芯片而言),速度的提高是出现在多核处理器的形式而不是更快的芯片。为了使程序运行得更快,你必须学会利用那些额外的处理器(译者注:处理器一般代表 CPU 的一个逻辑核心),这是并发所带来的好处之一。 对于多处理器机器,可以在这些处理器之间分配多个任务,这可以显著提高吞吐量。强大的多处理器 Web 服务器通常就是这种情况,它可以在程序中为 CPU 分配大量用户请求,每个请求分配一个线程。 -但是,并发性通常可以提高在单个处理器上运行的程序的性能。这听起来有点违反直觉。你会这么想,由于上下文切换的成本增加(从一个任务切换到另一个任务),在单个处理器上运行的并发程序实际上应该比程序的所有部分顺序运行具有更多的开销。表面上看,将程序的所有部分作为单个任务运行并节省上下文切换的成本似乎更低。 +但是,并发通常可以提高在单处理器上运行的程序的性能。这听起来有点违反直觉。如果你仔细想想,由于上下文切换的成本增加(从一个任务切换到另一个任务),在单个处理器上运行的并发程序实际上应该比程序的所有部分顺序运行具有更多的开销。从表面上看,将程序的所有部分作为单个任务运行,并且节省上下文切换的成本,这样看似乎更划算。 使这个问题变得有些不同的是阻塞。如果程序中的某个任务由于程序控制之外的某种情况而无法继续(通常是 I/O),我们就称该任务或线程已阻塞(在我们的科幻故事中,就是克隆人已经敲门并等待它打开)。如果没有并发,整个程序就会停下来,直到外部条件发生变化。但是,如果使用并发编写程序,则当一个任务被阻塞时,程序中的其他任务可以继续执行,因此整个程序得以继续运行。事实上,从性能的角度来看,如果没有任务会阻塞,那么在单处理器机器上使用并发是没有意义的。 单处理器系统中性能改进的一个常见例子是事件驱动编程,特别是用户界面编程。考虑一个程序执行一些耗时操作,最终忽略用户输入导致无响应。如果你有一个“退出”按钮,你不想在你编写的每段代码中都检查它的状态(轮询)。这会产生笨拙的代码,也无法保证程序员不会忘了检查。没有并发,生成可响应用户界面的唯一方法是让所有任务都定期检查用户输入。通过创建单独的线程以执行用户输入的响应,能够让程序保证一定程度的响应能力。 -实现并发的一种简单方式是使用操作系统级别的进程。与线程不同,进程是在其自己的地址空间中运行的独立程序。进程的优势在于,因为操作系统通常将一个进程与另一个进程隔离,因此它们不会相互干扰,这使得进程编程相对容易。相比之下,线程之间会共享内存和 I/O 等资源,因此编写多线程程序最基本的困难在于协调不同线程驱动的任务之间对这些资源的使用,以免这些资源不会同时被多个资源访问。 +实现并发的一种简单方式是使用操作系统级别的进程。与线程不同,进程是在其自己的地址空间中运行的独立程序。进程的优势在于,因为操作系统通常将一个进程与另一个进程隔离,因此它们不会相互干扰,这使得进程编程相对容易。相比之下,线程之间会共享内存和 I/O 等资源,因此编写多线程程序最基本的困难,在于协调不同线程驱动的任务之间对这些资源的使用,以免这些资源同时被多个任务访问。 有些人甚至提倡将进程作为唯一合理的并发实现方式[^1],但遗憾的是,通常存在数量和开销方面的限制,从而阻止了进程在并发范围内的适用性(最终你会习惯标准的并发限制,“这种方法适用于一些情况但不适用于其他情况”) -一些编程语言被设计为将并发任务彼此隔离。这些通常被称为_函数式语言_,其中每个函数调用不产生副作用(不会干扰到其它函数),所以可以作为独立的任务来驱动。Erlang 就是这样一种语言,它包括一个任务与另一个任务进行通信的安全机制。如果发现程序的某一部分必须大量使用并发,并且在尝试构建该部分时遇到了过多的问题,那么可以考虑使用这些专用的并发语言创建程序的这个部分。 +一些编程语言旨在将并发任务彼此隔离。这些通常被称为_函数式语言_,其中每个函数调用不产生副作用(不会干扰到其它函数),所以可以作为独立的任务来驱动。Erlang 就是这样一种语言,它包括一个任务与另一个任务进行通信的安全机制。如果发现程序的某一部分必须大量使用并发,并且在尝试构建该部分时遇到了过多的问题,那么可以考虑使用这些专用的并发语言创建程序的这个部分。 Java 采用了更传统的方法[^2],即在顺序语言之上添加对线程的支持而不是在多任务操作系统中分叉外部进程,线程是在表示执行程序的单个进程内创建任务。 -并发会带来各种成本,包括复杂性成本,但可以被程序设计,资源平衡和用户便利性方面的改进所抵消。通常,并发性使你能够创建更低耦合的设计;另一方面,你必须特别关注那些使用了并发操作的代码。 +并发会带来各种成本,包括复杂性成本,但可以被程序设计、资源平衡和用户便利性方面的改进所抵消。通常,并发性使你能够创建更低耦合的设计;另一方面,你必须特别关注那些使用了并发操作的代码。 ## Java 并发的四句格言 @@ -181,13 +181,13 @@ Java 采用了更传统的方法[^2],即在顺序语言之上添加对线程 ### 2.没有什么是真的,一切可能都有问题 -不使用并发编程,你已经料到了你的世界具有确定的顺序和一致性。对于变量赋值这样简单的操作,很明显它应该总是能够正常工作。 +不使用并发编程,你已经预料到你的世界具有确定的顺序和一致性。对于变量赋值这样简单的操作,很明显它应该总是能够正常工作。 在并发领域,有些事情可能是真的而有些事情却不是,以至于你必须假设没有什么是真的。你必须质疑一切。即使将变量设置为某个值也可能不会按预期的方式工作,事情从这里开始迅速恶化。我已经熟悉了这样一种感觉:我认为应该明显奏效的东西,实际上却行不通。 -所有在非并发编程中可以忽略的事情突然之间都因为并发而变得重要起来。例如,你必须了解处理器缓存以及保持本地缓存与主内存一致等问题。您必须理解对象构造的深层复杂性,以便构造函数不会意外地将数据暴露给其他线程更改。诸如此类问题不胜枚举。 +在非并发编程中你可以忽略的各种事情,在并发下突然变得很重要。例如,你必须了解处理器缓存以及保持本地缓存与主内存一致的问题,你必须理解对象构造的深层复杂性,这样你的构造函数就不会意外地暴露数据,以致于被其它线程更改。这样的例子不胜枚举。 -因为这些主题太复杂,以至于本章无法为你提供更专业的知识(再次参见 Java Concurrency in Practice),但你必须意识到它们。 +虽然这些主题过于复杂,无法在本章中给你提供专业知识(同样,请参见 Java Concurrency in Practice),但你必须了解它们。 ### 3.仅仅是它能运行,并不意味着它没有问题