Java Concurrency In Practice

Preface

开发测试并发程序很困难,因为并发BUG不会稳定复现,而且它们表面看起来可以运行,上了生产在高负荷下就会出现了。

用Java开发并发程序一个挑战是,平台提供的并发功能和开发者在程序里需要考虑的并发不匹配。Java提供了像synchronizedcondition wait这样的底层机制,但是这些机制需要一致地用来实现application-level的协议策略。没有这些策略,我们很容易编写编译看起来能正常工作的BUG程序。

Java 5.0 是Java并发发展的一大步。Java 5.0提供了新的高层组件和额外的底层机制让小白和专家更容易地编写并发程序。

我们的目标是给读者一些设计规约和思想模型来更加容易地编写正确,高性能的Java并发类/程序。

How to Use this Book

为了解决Java底层机制和设计层面策略需求之间的不匹配,我们提供了简化过了的规则来编写并发程序。可能会有专家说“这个规则不完整,C这个类虽然违反了规则R,但它依然是线程安全的”,违反我们的规则来编写正确的程序是有可能的,但是这样做需要对Java Memory Model有很深的理解,我们希望开发者不需要掌握这些细节也能写出正确的并发程序。

我们假设读者已经有一些Java关于并发的基础知识,没有的话可以阅读Java Programming Language关于线程的部分。

第一章的introduction之后,本书分为四个部分:
基础:Part 1(第二章到第五章)关注并发的基础概念和线程安全。如何不用类库提供的并发构件来编写线程安全类。

第二章(Thread Safety)和第三章(Sharing Objects)组成了这本书的基础部分。几乎所有避免并发陷阱,构建线程安全类,和验证线程是否安全的规则都在这里。相比理论,更喜欢实践的读者可以试着跳到Part 2,但是确保在写任何并发代码之前要回来读第二章和第三章。

第四章(Composing Objects)涵盖了把线程安全类组建成更大的线程安全类的技术。

第五章(Building Blocks)涵盖了平台库提供的并发构件(线程安全集合,synchronizers)。

构建并发程序。Part 2(第六章到第九章)描述了如何使用线程来提高并发程序的吞吐量和响应能力。第六章(Task Execution)覆盖了定义并行任务和用任务执行框架来执行他们。第七章(Cancellation and Shutdown)涉及让任务和线程在正常情况下终止的技术;程序如何处理取消和关闭常常是区分健壮的并发程序和仅仅只能工作程序的因素之一。第八章(线程池的应用)介绍了任务执行框架的高级特性。第九章(GUI 程序)着力于提高单线程子系统响应能力的技术。

活性、性能和测试。Part 3(章节10~12)关心保证并发程序按照你想的那样工作并且具备可接受的性能表现。第10章(规避活性陷阱)描述了如何避免让程序无法继续进行的活性失败。第11章(性能和拓展性)覆盖了提高并发程序的性能和拓展性的技术。第12章(测试并发程序)覆盖了测试并发程序正确性和性能的技术。

高级话题。Part 4(章节13-16)覆盖了一些可能经验丰富开发者才会感兴趣的话题:显式锁,原子变量,非阻塞算法和开发自定义synchronizer。

Chapter 1 ­ Introduction

写正确的程序很困难;写正确的并发程序更困难。相比串行程序,并发程序里有很多简单事情会出错。那么,为什么我们要用并发自寻烦恼?线程是Java里不可回避的特性,线程可以通过把复杂异步的代码转化成简单的直线代码,简化复杂系统的开发。另外,线程是压榨多处理器系统计算能力的最简单方式。并且,随着处理器个数的增加,更有效率地并发只会越来越重要。

1.1 A (Very) Brief History of Concurrency

在远古时代,计算机们还没有自己的操作系统。它们从头到尾执行一个程序,并且程序能够直接访问机器所有资源。在裸机上难于编写程序,而且一个只能运行一个程序没有充分利用稀缺昂贵的计算机资源。

进化的操作系统允许一次运行多个程序,在进程里独立地运行,由操作系统来分配资源,如内存,文件句柄,和安全证书。如果它们需要的话,进程可以通过粗粒度通信机制来通信:sockets、signal handler、shared memory、semaphore 和files。

促进操作系统的发展以支持多程序同时执行,有以下的因素:

资源利用率。程序们有时不得不等待输出/输出这样的外部操作,在等待的时候,计算机不能做其他的工作。在等待的时候让其他程序跑起来效率更高。

公平。多用户和多程序应该公平地申请机器资源。时间片轮转法相比执行完一个程序再执行下一个程序这种策略更好。

方便书写多个程序,每个程序执行一个单独的任务,必要的时候协调一下这种方式比把所有任务放在一个程序里更简单更合理。

在早期的时分系统里,每个进程都是虚拟的冯诺伊曼计算机。进程有自己存储指令和数据的内存空间,根据机器语言的语义顺序执行指令,并且通过I/O原语集借助操作系统同外界交互。对于每条指令执行完,都会定义“下一条指令”是什么,并且根据指令集的规则控制程序的执行顺序 。如今几乎所有广泛使用的计算机语言仍然遵循着这样的顺序编程模型,语言规范清楚地定义当一个给定的动作执行后,“接下来会是什么”。

顺序编程模型是直观自然的,因为它模拟了人类的工作方式:一次做一件事,按顺序做。起床、穿上你的睡衣,下楼喝茶。在编程语言里,这些真实世界每个操作是细粒度操作序列的一个抽象--开橱柜,选择一种味道的茶叶,量茶叶的量,放到壶里,再看烧水壶里有没有水,如果没有倒些水到水壶,然后放到炉子上,打开炉子,等待水烧开,等等,最后一步,等待水烧开,这包含一定程度的异步。当水加热的时候,你可以选择接下来做什么--仅仅等待,或者在这段做些其他事例如开始烤面包(另外一个异步任务),或者看报纸。但是需要留出一些注意力在水壶上。茶壶和面包机知道它们的产品通常使用在异步的场景里,所以当完成任务,它们会发出语音提示。找到串行和并行之间的平衡常常是有效率人士的特征之一,这同样适用于程序。

促进进程发展的因素(资源利用率,公平,方便)也促进了线程的发展。线程允许多个程序控制流共存在一个进程里。它们共享着进程级资源(如内存、文件句柄),但是每个线程有自己的PC、自己的栈和本地变量。线程也提供自然分解,以便在多处理器系统上利用硬件并行性;同一程序的多个线程在多CPU上可以同时被调度。

线程有时被称作轻量级进程,绝大多数的现代操作系统把线程,而不是进程作为调度的基本单位。如果没有显式地协调,线程们会同时运行,并且对于彼此都是异步的。由于线程共享所在进程的内存空间,所以同一进程里的所有线程都能够对同一个变量或者从同一个堆区分配的对象进行访问,也就是允许相比进程间粒度更小的数据共享。但是,没有显式同步协调对共享数据的访问,可能会发生一个线程在修改另外一个线程使用中的变量,这会导致不可预期的结果。

1.2 Benefits of Threads

合理利用线程可以降低开发和维护成本,提高复杂程序的性能。线程通过把异步流程转换成几乎串行的,可以更容易地模拟人类是如何工作和交互的。线程可以把复杂代码转换成更容易书写、阅读和维护的直线型代码。

线程也常常用在GUI应用里来提高用户界面的响应速度,也用在服务端程序里提高资源利用率和吞吐量。线程也可以用来简化JVM的实现--GC通常是一个或者多个线程常驻后台。绝大多数的优秀Java程序都会用到一定程度的多线程。

1.2.1. Exploiting Multiple Processors

多处理器系统曾经是昂贵、稀有的,一般用在大型数据中心、科学计算设施。如今,多处理系统随处可见。因为提高处理器的时钟频率比芯片上多放几个处理器核心要困难的多。

因为调度的基本单位是线程,单线程每个时刻只能在一个处理器上运行。在双核心系统里,单线程程序放弃了一半的CPU资源,在100核的系统中,单线程程序放弃了99%的处理器资源。从另一方面讲,一个程序的多个活动线程可以在多核心系统里同时执行,如果设计得当,多线程可以更有效率地利用空闲CPU资源来提高吞吐量。

你可能感兴趣的:(Java Concurrency In Practice)