From a2c447d5f8b20e05d6411f48a3f9e82dc73b5de8 Mon Sep 17 00:00:00 2001 From: blackwatch Date: Fri, 19 Jul 2019 10:29:30 +0800 Subject: [PATCH] The Rest of the Chapter and files --- docs/book/17-Files.md | 14 ++++-- docs/book/24-Concurrent-Programming.md | 64 +++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/docs/book/17-Files.md b/docs/book/17-Files.md index 82e785c..4ed54b6 100644 --- a/docs/book/17-Files.md +++ b/docs/book/17-Files.md @@ -2,17 +2,23 @@ # 第十七章 文件 -在丑陋的Java I/O编程方式诞生多年以后,Java终于简化了文件读写的基本操作。这种"困难方式"的全部细节都在[Appendix: I/O Streams]()。如果你读过这个部分,就会认同Java设计者毫不在意他们的使用者的体验这一观念。打开并读取文件对于大多数编程语言来是非常常用的,由于I/O糟糕的设计以至于 +>在丑陋的Java I/O编程方式诞生多年以后,Java终于简化了文件读写的基本操作。这种"困难方式"的全部细节都在 + +[Appendix: I/O Streams](.\Appendix-IO-Streams.md)。如果你读过这个部分,就会认同Java设计者毫不在意他们的使用者的体验这一观念。打开并读取文件对于大多数编程语言来是非常常用的,由于I/O糟糕的设计以至于 很少有人能够在不依赖其他参考代码的情况下完成打开文件的操作。 好像Java设计者终于意识到了Java使用者多年来的痛苦,在Java7中对此引入了巨大的改进。这些新元素被放在**java.nio.file**包下面,过去人们通常把**nio**中的**n**理解为**new**即新的**io**,现在更应该当成是**non-blocking**非阻塞**io**(**io**就是*input/output输入/输出*)。**java.nio.file**库终于将Java文件操作带到与其他编程语言相同的水平。最重要的是Java8新增的streams与文件结合使得文件操作编程变得更加优雅。我们将看一下文件操作的两个基本组件: + 1. 文件或者目录的路径; 2. 文件本身。 ## 文件和目录路径 + ### `Paths` + 一个**Path**对象表示一个文件或者目录的路径,是一个跨操作系统(OS)和文件系统的抽象,目的是在构造路径时不必关注底层操作系统,代码可以在不进行修改的情况下运行在不同的操作系统上。**java.nio.file.Paths**类包含一个重载方法**static get()**,该方法方法接受一系列**Strings**字符串或一个*统一资源标识符*(URI)作为参数,并且进行转换返回一个**Path**对象: + ```java // files/PathInfo.java import java.nio.file.*; @@ -24,7 +30,7 @@ public class PathInfo { static void show(String id, Object p) { System.out.println(id + ": " + p); } - + static void info(Path p) { show("toString", p); show("Exists", Files.exists(p)); @@ -38,7 +44,7 @@ public class PathInfo { } public static void main(String[] args) { System.out.println(System.getProperty("os.name")); - info(Paths.get("C:", "path", "to", "nowhere", "NoFile.txt")); + info(Paths.get("C:", "path", "to", "nowhere", "NoFile.txt")); Path p = Paths.get("PathInfo.java"); info(p); Path ap = p.toAbsolutePath(); @@ -116,7 +122,7 @@ true */ ``` -我已经在这一章第一个程序的**main()**方法添加了第一行用于展示操作系统的名称,因此你可以看到不同操作系统之间存在哪些差异。理想情况下,差别会相对较小,并且使用**/**或者**\\**路径分隔符进行分隔。你可以看到我运行在Windows 10上的程序输出。 +我已经在这一章第一个程序的 **main()** 方法添加了第一行用于展示操作系统的名称,因此你可以看到不同操作系统之间存在哪些差异。理想情况下,差别会相对较小,并且使用 **/** 或者 **\\** 路径分隔符进行分隔。你可以看到我运行在Windows 10上的程序输出。 当**toString()**方法生成完整形式的路径,你可以看到**getFileName()** 方法总是返回当前文件名。 通过使用**Files**工具类(我们接下类将会更多的使用它),可以测试一个文件是否存在,测试是否是一个"真正"的文件还是一个目录等等。"Nofile.txt"这个示例展示我们描述的文件可能并不在指定的位置;这样可以允许你创建一个新的路径。"PathInfo.java"存在于当前目录中,最初它只是没有路径的文件名,但它仍然被检测为"存在"。一旦我们将其转换为绝对路径,我们将会得到一个从"C:"盘(因为我们是在Windows机器下进行测试)开始的完整路径,现在它也拥有一个父路径。“真实”路径的定义在文档中有点模糊,因为它取决于具体的文件系统。例如,如果文件名不区分大小写,即使路径由于大小写的缘故而不是完全相同,也可能得到肯定的匹配结果。在这样的平台上,**toRealPath()** 将返回实际情况下的**Path**,并且还会删除任何冗余元素。 diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md index 642aef4..06856a7 100644 --- a/docs/book/24-Concurrent-Programming.md +++ b/docs/book/24-Concurrent-Programming.md @@ -212,7 +212,7 @@ Java采用了更传统的方法[^2],即在顺序语言之上添加对线程的 Java是一种多线程语言,如果您了解它们是否存在并发问题。因此,有许多Java程序正在使用中,或者只是偶然工作,或者大部分时间工作并且不时地发生问题,因为。有时这种问题是相对良性的,但有时它意味着丢失有价值的数据,如果你没有意识到并发问题,你最终可能会把问题放在其他地方而不是你的代码中。如果将程序移动到多处理器系统,则可以暴露或放大这些类型的问题。基本上,了解并发性使您意识到正确的程序可能会表现出错误的行为。 -## 残酷的真相 +## 残酷的真相 当人类开始烹饪他们的食物时,他们大大减少了他们的身体分解和消化食物所需的能量。烹饪创造了一个“外化的胃”,从而释放出追去其他的的能力。火的使用促成了文明。 @@ -248,14 +248,76 @@ Java实验告诉我们,结果是悄然灾难性的。程序员很容易陷入 这是我们将在本章的其余部分介绍的内容。请记住,本章的重点是使用最新的高级Java并发结构。使用这些使得您的生活比旧的替代品更加轻松。但是,您仍会在遗留代码中遇到一些低级工具。有时,你可能会被迫自己使用其中的一些。附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md)包含一些更原始的Java并发元素的介绍。 - Parallel Streams(并发流) +到目前为止,我已经强调了Java 8 Streams提供的改进语法。现在您对该语法(作为一个粉丝,我希望)感到满意,您可以获得额外的好处:您可以通过简单地将parallel()添加到表达式来并行化流。这是一种简单,强大,坦率地说是利用多处理器的惊人方式 + +添加parallel()来提高速度似乎是微不足道的,但是,唉,它就像你刚刚在[残酷的真相](#The-Brutal-Truth)中学到的那样简单。我将演示并解释一些盲目添加parallel()到Stream表达式的缺陷。 + - 创建和运行任务 +任务是一段可以独立运行的代码。为了解释创建和运行任务的一些基础知识,本节介绍了一种比并行流或CompletableFutures:Executor更复杂的机制。执行者管理一些低级Thread对象(Java中最原始的并发形式)。您创建一个任务,然后将其交给Executorto运行。 + +有多种类型的Executor用于不同的目的。在这里,我们将展示规范形式,代表创建和运行任务的最简单和最佳方法。 + - 终止长时间运行的任务 +任务独立运行,因此需要一种机制来关闭它们。典型的方法使用了一个标志,这引入了共享内存的问题,我们将使用Java的“Atomic”库来回避它。 - Completable Futures +当您将衣服带到干洗店时,他们会给您一张收据。你继续完成其他任务,最终你的衣服很干净,你可以拿起它。收据是您与干洗店在后台执行的任务的连接。这是Java 5中引入的Future的方法。 + +Future比以前的方法更方便,但你仍然必须出现并用收据取出干洗,等待任务没有完成。对于一系列操作,Futures并没有真正帮助那么多。 + +Java 8 CompletableFuture是一个更好的解决方案:它允许您将操作链接在一起,因此您不必将代码写入接口排序操作。有了CompletableFuture完美的结合,就可以更容易地做出“采购原料,组合成分,烹饪食物,提供食物,清理菜肴,储存菜肴”等一系列链式操作。 + - 死锁 +某些任务必须去**等待 - 阻塞**来获得其他任务的结果。被阻止的任务有可能等待另一个被阻止的任务,等待另一个被阻止的任务,等等。如果被阻止的任务链循环到第一个,没有人可以取得任何进展,你就会陷入僵局。 + +如果在运行程序时没有立即出现死锁,则会出现最大的问题。您的系统可能容易出现死锁,并且只会在某些条件下死锁。程序可能在某个平台上运行正常,例如您的开发机器,但是当您将其部署到不同的硬件时会开始死锁。 + +死锁通常源于细微的编程错误;一系列无辜的决定,最终意外地创建了一个依赖循环。本节包含一个经典示例,演示了死锁的特性。 + +我们将通过模拟创建披萨的过程完成本章,首先使用并行流实现它,然后是完成配置。这不仅仅是两种方法的比较,更重要的是探索你应该投入多少工作来加速计划。 + - 努力,复杂,成本 ## 并行流 +Java 8流的一个显着优点是,在某些情况下,它们可以很容易地并行化。这来自仔细的库设计,特别是流使用内部迭代的方式 - 也就是说,它们控制着自己的迭代器。特别是,它们使用一种特殊的迭代器,称为Spliterator,它被限制为易于自动分类。这产生了相当神奇的结果,只能说.parallel(),并且你的流中的所有东西都是作为一组并行任务运行的。如果您的代码是使用Streams编写的,那么并行化以提高速度似乎微不足道。 + +例如,考虑来自Streams的Prime.java。查找质数可能是一个耗时的过程,我们可以看到该程序的计时: + +```java +// concurrent/ParallelPrime.java +import java.util.*; +import java.util.stream.*; +import static java.util.stream.LongStream.*; +import java.io.*; +import java.nio.file.*; +import onjava.Timer; + +public class ParallelPrime { + static final int COUNT = 100_000; + public static boolean isPrime(long n){ + return rangeClosed(2, (long)Math.sqrt(n)).noneMatch(i -> n % i == 0); + } + public static void main(String[] args) + throws IOException { + Timer timer = new Timer(); + List primes = + iterate(2, i -> i + 1) + .parallel() // [1] + .filter(ParallelPrime::isPrime) + .limit(COUNT) + .mapToObj(Long::toString) + .collect(Collectors.toList()); + System.out.println(timer.duration()); + Files.write(Paths.get("primes.txt"), primes, StandardOpenOption.CREATE); + } + } + /* + Output: + 1224 + */ +``` + +请注意,这不是微基准测试,因为我们计时整个程序。我们将数据保存在磁盘上以防止激进的优化;如果我们没有对结果做任何事情,那么一个狡猾的编译器可能会观察到程序没有意义并且消除了计算(这不太可能,但并非不可能)。请注意使用nio2库编写文件的简单性(在[文件](./17-Files.md)一章中有描述)。 ## 创建和运行任务