diff --git a/new/java-proj/5.md b/new/java-proj/5.md index 11e7a91a6ac2c6dc35b40407a3f4a124413cf25a..cbedec2eb20d87aa7df1aebe6d5f3b8d30595f17 100644 --- a/new/java-proj/5.md +++ b/new/java-proj/5.md @@ -18,7 +18,7 @@ 旧的算法是遍历所有的变化,并试图找到与表的当前状态相匹配的猜测。假设当前检查的猜测是秘密,我们会得到与实际答案相同的答案吗?如果是的话,那么当前的猜测就是秘密,它和其他猜测一样好。 -更复杂的方法可以实现 [min-max 算法](https://en.wikipedia.org/wiki/Minimax)。这个算法不只是简单地得到下一个可能的猜测,而是查看所有可能的猜测,并选择一个最缩短游戏结果的猜测。如果有一个猜测在最坏的情况下可以再进行三次猜测,而另一个猜测的数字只有两次,那么 minmax 将选择后者。对于那些感兴趣的读者来说,实现 min-max 算法是一个很好的练习。在六种颜色和四列的情况下,最小-最大算法在不超过五个步骤的情况下解决游戏。我们实现的简单算法也分五步求解游戏。然而,我们没有朝这个方向走。 +更复杂的方法可以实现 [min-max 算法](https://en.wikipedia.org/wiki/Minimax)。这个算法不只是简单地得到下一个可能的猜测,而是查看所有可能的猜测,并选择一个最缩短游戏结果的猜测。如果有一个猜测在最坏的情况下可以再进行三次猜测,而另一个猜测的数字只有两次,那么 minmax 将选择后者。对于那些感兴趣的读者来说,实现 minmax 算法是一个很好的练习。在六种颜色和四列的情况下,最小-最大算法在不超过五个步骤的情况下解决游戏。我们实现的简单算法也分五步求解游戏。然而,我们没有朝这个方向走。 相反,我们希望有一个版本的游戏,利用一个以上的处理器。如何将算法转换为并行算法?这个问题没有简单的答案。当你有一个算法,你可以分析计算和部分算法,你可以尝试找到依赖关系。如果有一个计算,`B`需要数据,这是另一个计算,`a`的结果,那么很明显,`a`只能在`B`准备就绪时执行。如果算法的某些部分不依赖于其他部分的结果,那么它们可以并行执行。 @@ -304,9 +304,9 @@ public class Guess { } ``` -构造器正在创建作为参数传递的颜色数组的副本。因为 a`Guess`是不可变的,所以这是非常重要的。如果我们只保留原始数组,那么`Guess`类之外的任何代码都可能改变数组的元素,实质上改变了不应该改变的`Guess`的内容。 +构造器正在创建作为参数传递的颜色数组的副本。因为`Guess`是不可变的,所以这是非常重要的。如果我们只保留原始数组,那么`Guess`类之外的任何代码都可能改变数组的元素,实质上改变了不应该改变的`Guess`的内容。 -代码的下一部分是两个简单的 getter: +代码的下一部分是两个简单的获取器: ```java public Color getColor(int i) { @@ -446,7 +446,7 @@ Java 中代码的并行执行是在线程中完成的。您可能知道 Java 运 # 过程 -打开计算机电源后,启动的程序是操作系统(OS)。操作系统控制机器硬件和可以在机器上运行的程序。当你启动一个程序时,操作系统会创建一个新的进程。这意味着操作系统在一个表(数组)中分配一个新的条目,在这个表(数组)中它管理进程,并填充它知道的和需要知道的有关进程的参数。例如,它注册允许进程使用的内存段、进程的 ID、启动它的用户以及启动它的其他进程。你不能凭空开始一个过程。当你双击一个 EXE 文件时,你实际上告诉文件管理器(一个作为进程运行的程序)把 EXE 文件作为一个单独的进程启动。浏览器通过一个 API 调用系统,并请求操作系统这样做。操作系统将把 explorer 进程注册为新进程的父进程。此时操作系统实际上并不启动进程,而是创建它随后启动进程所需的所有数据,当有一些空闲的 CPU 资源时,进程启动,然后很快暂停,重新启动,然后暂停,依此类推。您不会注意到它,因为操作系统会一次又一次地启动它,并且总是反复暂停进程。它需要这样做才能为所有进程提供运行的可能性。这样,我们可以体验到所有进程同时运行。实际上,进程不会在单个处理器上同时运行,但它们经常会有时间段运行,因此我们感觉它们一直在运行。 +打开计算机电源后,启动的程序是操作系统(OS)。操作系统控制机器硬件和可以在机器上运行的程序。当你启动一个程序时,操作系统会创建一个新的进程。这意味着操作系统在一个表(数组)中分配一个新的条目,在这个表(数组)中它管理进程,并填充它知道的和需要知道的有关进程的参数。例如,它注册允许进程使用的内存段、进程的 ID、启动它的用户以及启动它的其他进程。你不能凭空开始一个过程。当你双击一个 EXE 文件时,你实际上告诉文件管理器(一个作为进程运行的程序)把 EXE 文件作为一个单独的进程启动。浏览器通过一个 API 调用系统,并请求操作系统这样做。操作系统将把资源管理器进程注册为新进程的父进程。此时操作系统实际上并不启动进程,而是创建它随后启动进程所需的所有数据,当有一些空闲的 CPU 资源时,进程启动,然后很快暂停,重新启动,然后暂停,依此类推。您不会注意到它,因为操作系统会一次又一次地启动它,并且总是反复暂停进程。它需要这样做才能为所有进程提供运行的可能性。这样,我们可以体验到所有进程同时运行。实际上,进程不会在单个处理器上同时运行,但它们经常会有时间段运行,因此我们感觉它们一直在运行。 如果计算机中有多个 CPU,那么进程实际上可以与有 CPU 的多个进程同时运行。随着集成的日益高级,台式计算机拥有包含多个核心的 CPU,它们几乎与单独的 CPU 一样运行。在我的机器上,我有四个内核,每个内核都能同时执行两个线程;所以,我的 MacOS 几乎就像一台 8 CPU 机器。当我开始工作时,一台 8 CPU 的电脑是一台价值百万美元的机器。 @@ -480,13 +480,13 @@ Java 版本 11 没有纤程,但是有一些库支持有限的纤程处理,[ 操作系统不知道是否使用了寄存器的值。寄存器中有位,只有看到处理器的状态,没有人能分辨出这些位是与当前代码执行相关,还是恰好以这种方式存在。编译器生成的程序确实知道哪些寄存器很重要,哪些寄存器可以忽略。这些信息在代码中的位置不同,但当需要交换机时,纤程会将需要在该点进行切换的信息传递给进行切换的代码。 -编译器计算这些信息,但 Java 在当前版本中不支持纤程。在 Java 中实现 fibers 的工具在编译阶段之后会分析和修改类的字节码。 +编译器计算这些信息,但 Java 在当前版本中不支持纤程。在 Java 中实现纤程的工具在编译阶段之后会分析和修改类的字节码。 -Golang 的 goroutine 是纤程类型,这就是为什么您可以轻松地在 Go 中启动数千个,甚至数百万个 goroutine 的原因,但是建议您将 Java 中的线程数限制为较低的数目。他们不是一回事。 +Golang 的 GoRoutine 是纤程类型,这就是为什么您可以轻松地在 Go 中启动数千个,甚至数百万个 GoRoutine 的原因,但是建议您将 Java 中的线程数限制为较低的数目。他们不是一回事。 尽管术语*轻量线程*正在慢慢消失,被越来越少的人使用,但纤程仍然经常被称为轻量线。 -# java.lang.Thread 线程 +# `Java.lang.Thread`线程 Java 中的一切(几乎)都是一个对象。如果我们想启动一个新线程,我们将需要一个对象,因此,一个代表线程的类。这个类是`java.lang.Thread`,它内置在 JDK 中。当您启动 Java 代码时,JVM 会自动创建一些`Thread`对象,并使用它们来运行它所需要的不同任务。如果您启动了 **VisualVM**,您可以选择任何 JVM 进程的线程选项卡,并查看 JVM 中的实际线程。例如,我启动的 VisualVM 有 29 个活动线程。其中一个是名为`main`的线程。这是一个开始执行`main`方法的方法(惊喜!)。`main`线程启动了大多数其他线程。当我们要编写一个多线程应用程序时,我们必须创建新的`Thread`对象并启动它们。最简单的方法是启动`new Thread()`,然后在线程上调用`start()`方法。它将开始一个新的`Thread`,它将立即结束,因为我们没有给它任何事情做。在 JDK 中,`Thread`类不执行我们的业务逻辑。以下是指定业务逻辑的两种方法: @@ -525,7 +525,7 @@ public class SimpleThreadIntermingling { } ``` -前面的代码创建两个线程,然后一个接一个地启动它们。当调用 start 方法时,它调度要执行的线程对象,然后返回。因此,当调用线程继续执行时,新线程将很快开始异步执行。在下面的示例中,两个线程和`main`线程并行运行,并创建如下输出: +前面的代码创建两个线程,然后一个接一个地启动它们。当调用`start`方法时,它调度要执行的线程对象,然后返回。因此,当调用线程继续执行时,新线程将很快开始异步执行。在下面的示例中,两个线程和`main`线程并行运行,并创建如下输出: ```java started t2 1, t2 2, t2 3, t2 4, t2 5, t2 6, t2 7, t2 8, t1 1, t2 9, t2 10, t2 11, t2 12,... @@ -563,7 +563,7 @@ t1.start(); 我们可以创建一个实现`Runnable`接口的类,而不是扩展`Thread`类来定义异步执行什么,这样做与*OO*编程方法更为一致。我们在类中实现的东西不是线程的功能。它更像是一种可以执行的东西。这是一个可以运行的东西。 -如果在不同的线程中执行是异步的,或者在调用 run 方法的同一个线程中执行,那么这是一个需要分离的不同关注点。如果这样做的话,我们可以将类作为构造器参数传递给一个`Thread`对象。对`Thread`对象调用`start`将启动我们传递的对象的 run 方法。这不是收益。好处是我们还可以将`Runnable`对象传递给`Executor`(可怕的名字,哈!)。`Executor`是一个接口,实现以高效的方式在`Thread`对象中执行`Runnable`(还有`Callable`,见下文)对象。执行者通常有一个准备就绪并处于`BLOCKED`状态的`Thread`对象池。当`Executor`有一个新任务要执行时,它将它交给`Thread`对象之一,并释放阻塞线程的锁。`Thread`进入`RUNNABLE`状态,执行`Runnable`,再次被阻塞。它不会终止,因此,可以在以后重用它来执行另一个`Runnable`。这样,`Executor`实现就避免了操作系统中线程注册的资源消耗过程。 +如果在不同的线程中执行是异步的,或者在调用`run`方法的同一个线程中执行,那么这是一个需要分离的不同关注点。如果这样做的话,我们可以将类作为构造器参数传递给一个`Thread`对象。对`Thread`对象调用`start`将启动我们传递的对象的`run`方法。这不是收益。好处是我们还可以将`Runnable`对象传递给`Executor`(可怕的名字,哈!)。`Executor`是一个接口,实现以高效的方式在`Thread`对象中执行`Runnable`(还有`Callable`,见下文)对象。执行者通常有一个准备就绪并处于`BLOCKED`状态的`Thread`对象池。当`Executor`有一个新任务要执行时,它将它交给`Thread`对象之一,并释放阻塞线程的锁。`Thread`进入`RUNNABLE`状态,执行`Runnable`,再次被阻塞。它不会终止,因此,可以在以后重用它来执行另一个`Runnable`。这样,`Executor`实现就避免了操作系统中线程注册的资源消耗过程。 专业应用程序代码从不创建新的`Thread`。应用程序代码使用框架来处理代码的并行执行,或者使用一些`ExecutorService`提供的`Executor`实现来启动`Runnable`或`Callable`对象。 @@ -616,7 +616,7 @@ t1.start(); `Runnable`接口定义了一个`run`方法。它没有参数,不返回值,也不引发异常。`Callable`接口是参数化的,它定义的唯一方法`call`没有参数,但返回泛型值,还可能抛出`Exception`。在代码中,如果我们只想运行某个东西,我们就实现了`Runnable`,如果我们想返回某个东西,我们就实现了`Callable`。这两个接口都是函数式接口;因此,它们是使用 Lambda 实现的很好的候选接口。 -为了获得一个`ExecutorService`实现的实例,我们可以使用实用类`Executors`。通常,当 JDK 中有一个`XYZ`接口时,可以有一个`XYZs`(复数)实用程序类,为接口的实现提供工厂。如果我们想多次启动`t1`任务,我们可以不创建新的`Thread`就这样做。我们应该使用以下 executor 服务: +为了获得一个`ExecutorService`实现的实例,我们可以使用实用类`Executors`。通常,当 JDK 中有一个`XYZ`接口时,可以有一个`XYZs`(复数)实用程序类,为接口的实现提供工厂。如果我们想多次启动`t1`任务,我们可以不创建新的`Thread`就这样做。我们应该使用以下执行器服务: ```java public class ThreadIntermingling { @@ -703,13 +703,13 @@ completable future 类实现了`Future`接口,但它还提供了其他方法 CompletableFuture.supplyAsync( supply_value ).thenAcceptAsync( consume_the_value ) ``` -它说启动由`supply_value`表示的`Supplier`,当它完成时,将这个值提供给由`consume_the_value`表示的消费者。示例代码计算 pi 的值并提供该值。`consume_the_value`部分将值打印到输出。当我们运行代码时,文本`All is scheduled`可能会首先打印到输出中,然后才打印 pi 的计算值。 +它说启动由`supply_value`表示的`Supplier`,当它完成时,将这个值提供给由`consume_the_value`表示的消费者。示例代码计算 PI 的值并提供该值。`consume_the_value`部分将值打印到输出。当我们运行代码时,文本`All is scheduled`可能会首先打印到输出中,然后才打印 PI 的计算值。 类还实现了许多其他方法。当完全未来不产生任何价值或者我们不需要消耗价值时,我们应该使用`thenRunAsync(Runnable r)`方法。 -如果我们想消费价值,同时又想从中创造新的价值,那么我们应该使用`thenApplyAsync()`方法。此方法的参数是一个`Function`,它获取运行后可完成 future 的结果,结果是可完成 future`thenApplyAsync()`返回的值。 +如果我们想消费价值,同时又想从中创造新的价值,那么我们应该使用`thenApplyAsync()`方法。此方法的参数是一个`Function`,它获取运行后`CompletableFuture`的结果,结果是`CompletableFuture thenApplyAsync()`返回的值。 -在可完成的未来完成之后,还有许多其他方法执行代码。所有这些都用于在第一个可完成的将来完成后指定某个回调。完全未来代码的执行可能引发异常。在这种情况下,可完成的未来就完成了;它不会抛出异常。异常被捕获并存储在 completable future 对象中,只有当我们想访问调用`get()`方法的结果时才会抛出异常。方法`get()`抛出一个封装原始异常的`ExecutionException`。`join()`方法抛出原始异常。 +在可完成的未来完成之后,还有许多其他方法执行代码。所有这些都用于在第一个可完成的将来完成后指定某个回调。完全未来代码的执行可能引发异常。在这种情况下,可完成的未来就完成了;它不会抛出异常。异常被捕获并存储在`CompletableFuture`对象中,只有当我们想访问调用`get()`方法的结果时才会抛出异常。方法`get()`抛出一个封装原始异常的`ExecutionException`。`join()`方法抛出原始异常。 像`thenAcceptAsync()`这样的方法有它们的同步对,例如`thenAccept()`。如果调用此函数,则将执行传递的代码: @@ -733,7 +733,7 @@ consume_the_value( cf.get() ) 使用`CompletableFuture`的最佳用例是当我们进行异步计算并且需要回调方法来处理结果时。 -# ForkJoinPool 公司 +# `ForkJoinPool` `ForkJoinPool`是一个特殊的`ExecutorService`,它有执行`ForkJoinTask`对象的方法。当我们要执行的任务可以被分解成许多小任务,然后当结果可用时,这些类非常方便。使用这个执行器,我们不需要关心线程池的大小和关闭执行器。线程池的大小根据给定机器上的处理器数量进行调整,以获得最佳性能。因为`ForkJoinPool`是一个特殊的`ExecutorService`是为短期运行的任务而设计的,所以它不希望有任何任务在那里停留更长的时间,也不希望在没有更多任务要运行时需要任何任务。因此,它作为守护线程执行;当 JVM 关闭时,`ForkJoinPool`自动停止。 @@ -813,13 +813,13 @@ public class VolatileDemonstration implements Runnable { 它可能会在一些 Java 实现上停止,但在大多数实现中,它只会继续旋转。原因是 JIT 编译器优化了代码。它看到循环什么也不做,而且变量永远不会是非空的。允许假设因为没有声明为`volatile`的变量不应该被不同的线程修改,所以 JIT 可以进行优化。如果我们将`Object o`变量声明为`volatile`,那么代码将停止。您还必须删除`final`关键字,因为变量不能同时是`final`和`volatile`。 -如果您试图删除对 sleep 的调用,代码也将停止。然而,这并不能解决这个问题。原因是 JIT 优化只在大约 5000 次代码执行循环之后才开始。在此之前,代码运行简单,并在优化之前停止,这将消除对非易失性变量的额外访问(通常不需要)。 +如果您试图删除对`sleep`的调用,代码也将停止。然而,这并不能解决这个问题。原因是 JIT 优化只在大约 5000 次代码执行循环之后才开始。在此之前,代码运行简单,并在优化之前停止,这将消除对非易失性变量的额外访问(通常不需要)。 如果这是如此可怕,那么为什么我们不声明所有变量都是易变的呢?为什么 Java 不能为我们做到这一点?答案是速度。为了更深入地理解这一点,我们将用办公室和官僚来比喻。 # CPU 心跳 -现在,CPU 在 2 到 4GHz 频率的处理器上运行。这意味着处理器每秒得到 2 到 4 倍于 109的时钟信号来做某事。处理器不能执行比这更快的任何原子操作,而且也没有理由创建一个比处理器可以遵循的更快的时钟。这意味着 CPU 在半纳秒或四分之一纳秒内执行一个简单的操作,例如递增寄存器。这是处理器的心跳,如果我们认为官僚是人,他们是谁,那么它相当于一秒钟,大约,他们的心跳。在我们的想象中,这会将计算机的运行速度减慢到可以理解的速度。 +现在,CPU 在 2 到 4GHz 频率的处理器上运行。这意味着处理器每秒得到 2 到 4 倍于`10**9`的时钟信号来做某事。处理器不能执行比这更快的任何原子操作,而且也没有理由创建一个比处理器可以遵循的更快的时钟。这意味着 CPU 在半纳秒或四分之一纳秒内执行一个简单的操作,例如递增寄存器。这是处理器的心跳,如果我们认为官僚是人,他们是谁,那么它相当于一秒钟,大约,他们的心跳。在我们的想象中,这会将计算机的运行速度减慢到可以理解的速度。 处理器在芯片上有不同级别的寄存器和高速缓存;L1、L2,有时还有 L3;还有存储器、SSD、磁盘、磁盘、网络和磁带,可能需要它们来检索数据。 @@ -827,11 +827,11 @@ public class VolatileDemonstration implements Runnable { 主存读取为 100ns。官僚站起来,走到靠墙的共享文件库,等着其他官僚把文件拿出来或放回去,选择抽屉,把它拿出来,拿着文件,走回办公桌。这需要两分钟。这是一种易变的变量访问,每次你在一个文档上写一个单词,它必须做两次,一次读,一次写,即使你碰巧知道下一件事就是在同一张纸上填写表单的另一个字段。 -现代架构没有多个 cpu,而是有多个核的单个 cpu,速度要快一些。一个内核可以检查另一个内核的缓存,以查看是否对同一变量进行了任何修改。这将 volatile 访问加速到 20ns 左右,这仍然比 non-volatile 慢一个数量级。 +现代架构没有多个 cpu,而是有多个核的单个 cpu,速度要快一些。一个内核可以检查另一个内核的缓存,以查看是否对同一变量进行了任何修改。这将易变访问加速到 20ns 左右,这仍然比非易变慢一个数量级。 尽管其余部分不太关注多线程编程,但这里值得一提,因为它很好地理解了不同的时间量级。 -从 SSD 读取一个块(通常为 4k 块)需要 150000ns。以人类的速度,这比 5 天多一点。在 Gb 本地以太网上通过网络向服务器读取或发送数据需要 0.5 毫秒,这就好像是在等一个月的时间。如果网络上的数据是在一个旋转的磁盘上,那么 seek 时间加起来(直到磁盘旋转以使磁表面的一部分进入读取头下的时间)为 20ms。对于在我们的计算环境中来回运行的想象中的小官僚来说,这大约是一年。 +从 SSD 读取一个块(通常为 4k 块)需要 150000ns。以人类的速度,这比 5 天多一点。在 Gb 本地以太网上通过网络向服务器读取或发送数据需要 0.5 毫秒,这就好像是在等一个月的时间。如果网络上的数据是在一个旋转的磁盘上,那么寻道时间加起来(直到磁盘旋转以使磁表面的一部分进入读取头下的时间)为 20ms。对于在我们的计算环境中来回运行的想象中的小官僚来说,这大约是一年。 如果我们在互联网上通过大西洋发送一个网络数据包,大约需要 150 毫秒。这就像 14 年,而这仅仅是一个数据包;如果我们要通过海洋发送数据,这将构成数千年的历史。如果我们计算一台机器启动一分钟,它相当于我们整个文明的时间跨度。 @@ -845,7 +845,7 @@ public class VolatileDemonstration implements Runnable { private volatile Object o = null; ``` -前面的代码运行良好,大约一秒钟后停止。任何 Java 实现都必须保证多个线程可以访问`volatile`字段,并且该字段的值是一致更新的。这并不意味着 volatile 声明将解决所有的同步问题,而是保证不同的变量及其值变化关系是一致的。例如,让我们考虑在一个方法中增加以下两个字段: +前面的代码运行良好,大约一秒钟后停止。任何 Java 实现都必须保证多个线程可以访问`volatile`字段,并且该字段的值是一致更新的。这并不意味着`volatile`声明将解决所有的同步问题,而是保证不同的变量及其值变化关系是一致的。例如,让我们考虑在一个方法中增加以下两个字段: ```java private int i=0,j=0; @@ -913,7 +913,7 @@ public class SynchronizedDemo implements Runnable { 代码从两个不同的线程开始。其中一个线程将`aa`附加到名为`sb`的`StringBuffer`上。另一个附加`bb`。这个附加操作分两个阶段进行,中间是睡眠。睡眠是为了避免 JIT 将两个单独的步骤优化为一个步骤。每个线程执行`append`1000 次,每次追加`a`或`b`两次。由于两个`append`一个接一个,并且它们在`synchronized`块内,所以`aba`或`bab`序列不可能进入`StringBuffer`中。当一个线程执行同步块时,另一个线程不能执行它。 -如果我删除 synchronized 块,那么我用来测试 Java HotSpot(TM)64 位服务器 VM 的 JVM(build 9-ea+121,mixed mode 和 18.3 build 10+46,mixed mode for the second edition of the book)打印出失败,try count 大约为几百次。(看看 Packt 提供的代码库中的`SynchronizedDemoFailing`类。) +如果我删除`synchronized`块,那么我用来测试 Java HotSpot(TM)64 位服务器 VM 的 JVM(对于本书的第二版,构建 9-ea+121,混合模式和 18.3 b构建 10+46,混合模式)打印出失败,尝试数量大约为几百次。(看看 Packt 提供的代码库中的`SynchronizedDemoFailing`类。) 它清楚地说明了同步意味着什么,但它也将我们的注意力吸引到另一个重要的现象上。错误只发生在大约每几十万次执行中。这是极为罕见的,即使这个例子是用来证明这样的灾难。如果一个 bug 很少出现,那么很难重现,甚至更难调试和修复。大多数同步错误都以神秘的方式表现出来,修复它们通常是仔细检查代码而不是调试的结果。因此,在启动商业多线程应用程序之前,清楚地了解 Java 多线程行为的真正本质是非常重要的。 @@ -973,7 +973,7 @@ a.lock(); b.lock(); a.unlock(); c.lock() 问题不在于执行。库的开发人员决定了这个规则,并不是因为他们喜欢这样,也不是因为他们知道并行算法和死锁的可能性。当两个线程有`readLock`并且每个线程都决定将锁升级到`writeLock`时,它们本质上会创建死锁。每个人都会在等待`writeLock`的时候拿着`readLock`,没有人会得到它。 -另一方面,您可以将 a`writeLock`降级为 a`readLock`,而无需冒风险,同时,有人获得 a`writeLock`并修改资源。 +另一方面,您可以将`writeLock`降级为`readLock`,而无需冒风险,同时,有人获得`writeLock`并修改资源。 # 原子变量 @@ -997,7 +997,7 @@ boolean compareAndSet(expectedValue, updateValue); 在`AtomicXXXArray`的情况下,方法有一个额外的第一个参数,它是调用中处理的数组元素的索引。 -就运行在不同处理器内核上的多个线程的重新排序和访问而言,封装的变量的处理方式与 volatile 相同。原子类的实际实现可能使用特殊的硬件代码,这些代码可以提供比 Java 中的原始实现更好的性能,因此原子类可能比使用易失性变量和同步块的普通 Java 代码中实现的相同功能具有更好的性能。 +就运行在不同处理器内核上的多个线程的重新排序和访问而言,封装的变量的处理方式与`volatile`相同。原子类的实际实现可能使用特殊的硬件代码,这些代码可以提供比 Java 中的原始实现更好的性能,因此原子类可能比使用易失性变量和同步块的普通 Java 代码中实现的相同功能具有更好的性能。 一般的建议是,如果有可用的原子类,可以考虑使用原子类,您将发现自己正在为检查和设置、原子增量或加法操作创建一个同步块。 @@ -1018,11 +1018,11 @@ boolean compareAndSet(expectedValue, updateValue); | **弹出** | `remove()` | `poll()` | `take()` | `poll(time, unit)` | | **检查** | `element()` | `peek()` | `not applicable` | `not applicable` | -# LinkedBlocking 队列 +# `LinkedBlockingQueue` 这是`BlockingQueue`接口的一个实现,它由一个链表备份。默认情况下,队列的大小不受限制(准确地说,它是`Integer.MAX_VALUE`),但是可以选择在构造器参数中进行限制。在这个实现中限制大小的原因是,当并行算法在有限大小的队列中执行得更好时,可以帮助使用。实现本身对大小没有任何限制,只有`Integer.MAX_VALUE`比较大。 -# LinkedBlockingDeque 链接 +# `LinkedBlockingDeque` 这是`BlockingQueue`及其`BlockingDeque`子接口的最简单实现,如前一章所述,`Deque`是一个双端队列,具有`add`、`remove`、`offer`等方法类型,以`xxxFirst`和`xxxLast`的形式与队列的一端或另一端执行动作。`Deque`接口定义了`getFirst`和`getLast`,而不是一致地命名`elementFirst`和`elementLast`,所以这是你应该习惯的。毕竟,IDE 有助于自动完成代码,所以这应该不是什么大问题。 @@ -1040,7 +1040,7 @@ boolean compareAndSet(expectedValue, updateValue); 在我们开始之前,我必须承认这个任务不是一个典型的并行编程教程任务。讨论并发编程技术的教程倾向于选择易于使用并行代码解决且可扩展性好的问题作为示例。如果在`N`处理器上运行的并行算法实际运行的速度是非并行解的`N`倍,那么问题就可以很好地扩展。我个人的看法是,这些例子描绘的天空蓝色没有风暴云。然而,当你面对现实生活中的并发编程时,那些云彩就在那里,你会看到雷声和闪电,如果你没有经验,你会大惊小怪的。 -现实生活中的问题往往规模不理想。我们已经访问了一个扩展性很好的示例,尽管它不是理想的快速排序。这一次,我们将为更接近现实问题的问题开发一个并行算法。在 N 个处理器上解算智囊团游戏不会使解算速度提高`N`倍,而且代码也不平凡。这个例子将向您展示现实生活中的问题是什么样子的,尽管它不会教您所有可能的问题,但是当您在商业环境中第一次看到其中一个问题时,您不会感到震惊。 +现实生活中的问题往往规模不理想。我们已经访问了一个扩展性很好的示例,尽管它不是理想的快速排序。这一次,我们将为更接近现实问题的问题开发一个并行算法。在`N`个处理器上解算智囊团游戏不会使解算速度提高`N`倍,而且代码也不平凡。这个例子将向您展示现实生活中的问题是什么样子的,尽管它不会教您所有可能的问题,但是当您在商业环境中第一次看到其中一个问题时,您不会感到震惊。 这个解决方案中最重要的类之一是`IntervalGuesser`。这是影响创建猜测的类。它在开始猜测和结束猜测之间创建猜测,并将它们发送到`BlockingQueue`。类实现了`Runnable`,因此可以在单独的`Thread`中运行。纯粹主义的实现将`Runnable`功能与区间猜测分开,但是,由于整个类几乎不超过 50 行,在单个类中实现这两个功能是可以原谅的错误: @@ -1204,7 +1204,7 @@ private void stopAsynchronousGuessers(IntervalGuesser[] guessers) { 微基准是另一回事。它是关于一个小的 Java 代码片段的性能,因此更接近于 Java 编程。 -它很少使用,在开始为实际商业环境执行微基准之前,我们必须三思而后行。Microbenchmark 是一个诱人的工具,可以在不知道是否值得优化代码的情况下优化一些小东西。当我们有一个在多个服务器上运行多个模块的大型应用程序时,我们如何确保改进应用程序的某个特殊部分能够显著提高性能?它是否会回报增加的收入,产生如此多的利润,以弥补性能测试和开发中产生的成本?从统计学上讲,你几乎可以肯定,这样的优化,包括微基准,不会有回报。 +它很少使用,在开始为实际商业环境执行微基准之前,我们必须三思而后行。MicroBenchmark 是一个诱人的工具,可以在不知道是否值得优化代码的情况下优化一些小东西。当我们有一个在多个服务器上运行多个模块的大型应用程序时,我们如何确保改进应用程序的某个特殊部分能够显著提高性能?它是否会回报增加的收入,产生如此多的利润,以弥补性能测试和开发中产生的成本?从统计学上讲,你几乎可以肯定,这样的优化,包括微基准,不会有回报。 我曾经维护过一位资深同事的密码。他创建了一个高度优化的代码来识别文件中存在的配置关键字。他创建了一个程序结构,它表示基于键字符串中的字符的决策树。如果配置文件中有一个关键字拼写错误,代码会在第一个字符处抛出异常,从而确定关键字不正确。要插入一个新的关键字,它需要通过代码结构来找到新关键字最初与已有关键字不同的地方,并扩展深度嵌套的`if/else`结构。阅读关键字列表处理是可能的,从评论中列出了所有的关键字,他没有忘记文件。代码运行速度惊人,可能节省了 Servlet 应用程序几毫秒的启动时间。应用程序仅在每隔几天进行一次系统维护之后才启动几个月。你呢感受一下讽刺吧?资历并不总是年数。那些更幸运的人可以拯救他们内心的孩子。 @@ -1227,9 +1227,9 @@ private void stopAsynchronousGuessers(IntervalGuesser[] guessers) { 让我们记住官僚们的心跳,以及从记忆中读东西所需要的时间。打电话给一个方法,比如`System.nanoTime(),`,就像让酒店的行李员从二楼跑到大堂,往外看一眼路对面塔楼上的钟,回来,准确地告诉我们询问的时间。胡说。我们应该知道塔台上的钟的精确度,以及行李员从地板到大堂和大厅的速度。这不仅仅是打电话给`System.nanoTime()`。这就是微型标记装置为我们所做的。 -*“Java 微基准线束”*(**JMH**)作为库提供了一段时间。它是由 Oracle 开发的,用于调整几个核心 JDK 类的性能。这对那些为新硬件开发 Java 平台的人来说是个好消息,但对开发人员来说也是个好消息,因为这意味着 JMH 现在和将来都会受到 Oracle 的支持。 +**Java 微基准线束**(**JMH**)作为库提供了一段时间。它是由 Oracle 开发的,用于调整几个核心 JDK 类的性能。这对那些为新硬件开发 Java 平台的人来说是个好消息,但对开发人员来说也是个好消息,因为这意味着 JMH 现在和将来都会受到 Oracle 的支持。 -“*JMH 是一个 Java 工具,用于构建、运行和分析以 Java 编写的 nano/micro/mili/macro 基准,以及其他针对 JVM 的语言。* +“JMH 是一个 Java 工具,用于构建、运行和分析以 Java 编写的 nano/micro/mili/macro 基准,以及其他针对 JVM 的语言。” (引自 [JMH 官方网站](http://openjdk.java.net/projects/code-tools/jmh/))。 @@ -1333,7 +1333,7 @@ playSimple N/A N/A 18,613 ± 2,040 关于 Java 提供的不同同步方式有一个很大的话题,您还了解了程序员在编写并发应用程序时可能遇到的陷阱。 -最后,但并非最不重要的是,我们创建了 Mastermind guesser 的并发版本,并且还测量了它确实比只使用一个处理器的版本(至少在我的机器上)要快。我们在 Gradle 构建工具中使用了 javamicrobenchmark 工具,并讨论了如何执行 microbenchmark。 +最后,但并非最不重要的是,我们创建了 Mastermind 猜测器的并发版本,并且还测量了它确实比只使用一个处理器的版本(至少在我的机器上)要快。我们在 Gradle 构建工具中使用了 JavaMicroBenchmark 工具,并讨论了如何执行微基准。 这是一个漫长的章节,并不容易。我可能倾向于认为这是最复杂和理论的一章。如果你一开始就理解了一半,你会感到骄傲的。另一方面,请注意,这仅仅是一个坚实的基础,可以从中开始试验并发编程,在被公认为该领域的经验丰富和专业人士之前,还有很长的路要走。而且,这一章也不容易。但是,首先,在这一章的结尾,要为自己感到骄傲。