第 9 章 并行与并发
这一章将讨论 Java 8 中的并行(parallelization)和并发(concurrency)。某些概念可以追溯到早期版本(特别是 Java 5 引入的 java.util.concurrent 包),不过 Java 8 对并行和并发进行了强化,使得开发人员可以在更高的抽象层次上操作。
在讨论并行和并发时,有人会非常在意这两个术语之间的区别。我们对二者做一简单定义。
- 并发:多个任务可以在重叠的时间段内运行。
- 并行:多个任务可以同时运行。
并发设计指将一个任务分解为多个能同时运行的独立操作——即便目前尚未这样处理。换言之,并发应用程序由若干独立执行的进程组成。如果存在多个处理单元,就可以并行地实现这些并发任务,但性能是否会因此而提升将视情况而定 1。
1Go 语言之父 Rob Pike 在题为“Concurrency Is Not Parallelism”的演讲中,对这两个概念做了精彩且扼要的解释。感兴趣的读者可以观看上传到 YouTube 的演讲视频。
那么,并行为什么在某些情况下无助于性能提升呢?原因是多方面的。对 Java 而言,并行在默认情况下将任务分解成多个子任务,每个子任务被分配给通用 fork/join 线程池(common fork/join pool)并执行,最后将所有结果合并在一起。但是,所有操作都会引入开销,而很多预期的性能提升取决于问题映射到算法的程度。对于是否使用并行,不妨参考本章范例给出的指导方针。
Java 8 使并行变得简单易行。Clojure 语言之父 Rich Hickey 在 Strange Loop 2011 上以“Simple Made Easy”为题,对此做了精彩的阐述。Hickey 表示,一个基本概念是简单(simple)和容易(easy)这两个词具有不同的含义。简而言之,简单的事物在概念上并无歧义,而容易的事物在看似容易的背后或许隐藏了巨大的复杂性。例如,有些排序算法很简单,有些则不然,但调用 Stream.sorted 方法总是很容易 2。
2在《星际迷航:下一代》中,饰演皮卡德舰长(Captain Picard)的帕特里克 • 斯图尔特(Patrick Stewart)为“简单”与“容易”做了另一个很好的注脚:编剧试图向斯图尔特描述进入行星轨道所需的全部详细步骤,斯图尔特答道:“何必这么繁琐?告诉我‘标准轨道,少尉’就够了。”
并行和并发处理涉及多方面的内容,是一个令人颇感头疼的话题。Java 在面世之初就引入了支持多线程访问的底层机制,如 Object 类定义的 wait、notify、notifyAll 方法以及 synchronized 关键字。但使用这样的原语(primitives)实现并发难如登天,因此 Java 5 引入了 java.util.concurrent 包。借由 ExecutorService、BlockingQueue 接口以及 ReentrantLock 类,开发人员得以在更高的抽象层次上处理并发。尽管如此,并发管理仍然不是一项轻松的工作,特别是遇到堪称梦魇的“共享可变状态”(shared mutable state)时。
而在 Java 8 中,请求并行流不再是难事,因为只需进行一次方法调用。问题在于,性能提升并不简单。之前存在的所有问题其实仍然存在,它们只是被隐藏在表面之下而已。
并发和并行的讨论分散于全书,这一章的范例无法涵盖全部内容。3 本章旨在介绍各种可用的机制及其用法。掌握这些知识和概念后,读者可以将它们应用到开发中,并根据实际情况决定是否使用并发和并行。
3感兴趣的读者可以阅读 Brian Goetz 撰写的 Java Concurrency in Practice 一书(由 Addison-Wesley Professional 于 2006 年 5 月出版),以及 Venkat Subramaniam 撰写的 Programming Concurrency on the JVM 一书(由 Pragmatic Bookshelf 于 2011 年 9 月出版)。
