参考了很多童老师团队的翻译,自己也看英文对照,有修改
章节一:
写一个正确的程序,难,写一个正确的并发程序,难上加难。相比于非并发程序(个人理解是单线程那种的),并发程序里有更多容易产生错误的地方。那么为啥我们还要这么费劲研究并发呢?因为线程Thread是java编程语言里谁都躲不掉的功能啊,而且Threads还能通过把复杂异步代码转换成直线式程序(这里应该是指换成并发运行的代码块?),简化复杂系统的开发。此外,线程Threads也是能够挖掘发挥多处理器系统(多核)计算能力最简单的方法。随着CPU的增加更有效利用并发肯定会变得越来越重要。
1.1并发发展简史
在计算机历史中的远古时代,电脑都没有操作系统;他们从头到尾执行一个单个的程序,并且这个程序能直接接触到计算机机器上所有资源。这就造成了以下结果:在暴露所有一切给你的机器上去写个能运行的程序很困难,考虑到昂贵稀缺的电脑资源,一次跑一个单个程序也是效率很低的事情。
操作系统发展起来后,允许不止一个程序在进程中即刻独立的运行:操作系统为他们分配诸如存储、文件句柄、安全凭证等资源,保证他们独立的运行。如果有需要的话,进程也能够通过一种粗粒度的交流机制,和其他进程进行交流,像sockets套接字、信号处理程序、共享存储、信号灯和文件。
几种积极的激励因素促使操作系统不断发展,来允许更多的程序同时去执行:
资源利用率--程序有时候不得不等待诸如输入输出的额外操作,在等待期间不能做什么有用的工作。这种等待的时间如果能用来执行别的程序当然是更有效率的。
公平性--多用户或者多程序可能对计算机资源有相同的要求。相比于让一个程序跑完再让另一个程序开始运行,通过更细粒度的时间切片去划分谁在哪个极微小时间单位内占有电脑资源,这样显然效率更高。
方便性--与写一个单独的程序跑所有的任务相比,写几个程序,然后每个程序运行一个单独的任务并在必要的时候协调调用,显然要简单或者说更值得去这样做。
在早期的分时系统中,每个进程都是一台虚拟的冯诺依曼计算机;每个进程都有一个存储空间,存储着所有的指令和数据。这些顺序执行指令根据机器语言的语义所写,指令通过在操作系统中操作一套I/O原语(原函数)和外界进行互动。对于每一个被执行的指令,都有明确定义的“下一条指令”,并且控制流程会根据指令集合定义的规则跑完程序。今天近乎所有的被广泛应用的编程语言都遵循这个程序顺序执行模型,语言规范里清楚定义了在给出的任务执行后“下一步去干啥”。
程序顺序执行模型是直观自然的,它就像是人类工作的方式:一段时间内按着顺序走就干一个事------大多数情况下。比如起床,穿浴衣,下楼,喝茶去。在程序语言中,这些真实世界的每一个活动就是程序里顺序执行行为的抽象物。比如说打开橱柜,选择一种品牌的茶,估计一下到多少茶叶,看下茶壶里水够不够,不够添点,把火炉打开,等茶水煮开了喝等等。像这最后一步操作--等茶水煮开的过程中---就涉及到了异步。当水在加热时,你可以选择去干啥---傻了吧唧等着或者在这段时间内干别的事,比如开始烤面包(另一个异步任务)或者看看报纸,当然都要有一点注意力给你的茶壶,有需要的话赶紧回来弄。茶壶和面包机的制造商知道他们的产品总会在一些异步行为情况下被使用,所以他们设计产品的时候让它能够在完成任务的时候发出声音信号。找到同步和异步的平衡点是一个高效的人的特点,对程序来说也一样。
同样的关注(资源利用率,公平性,方便性等方面)促进了进程的发展,也同样促进了线程的发展。线程Thread允许在一个进程中共存多个程序控制执行流程(多个streams)。他们共享进程范围内的资源,比如说存储,文件句柄。但是每个线程都有自己的程序计数器、栈和本地变量。在多处理器系统中线程们也能利用硬件的并行性能力自然地分解开来;在相同程序里的多个线程能够在多CPU中同步调度执行。
线程有时也被称作轻量级的进程,而且大多数现在操作系统都把线程作为调度的基本单位,而不是进程。在没有明确协调的时候,线程同步和异步的执行都会考虑到对方。从线程开始共享他们在所属进程的存储地址空间,同一进程的所有的线程都能获得同一变量、从同一堆中分配到对象,这些方式比进程间机制更细粒化的共享了数据。但是如果没有明确的同步规范来控制协调对共享数据的操作的话,一个线程可能会在另一个线程使用某个变量的过程中进行修改,对造成不可预料的后果。
1.2从线程中受益
如果能够合理地使用多线程,将能够缩减复杂应用程序的开发和维护成本,并能提供更好的性能。通过将异步工作流转换为多个序列化工作流,多线程可以更好地对人类的工作和交互方式建模。使用多线程,很多复杂的代码将变得更加直截了当,因此更容易编写、阅读和维护。 在图形用户界面程序中使用多线程可以提高界面响应速度,在服务器程序中使用多线程可以提高资源利用率和吞吐量。多线程还可以简化 JVM 的设计,垃圾回收器一般在一个专用线程中工作。大多数卓越的 Java 应用程序都在某种程度上依赖于多线程。
1.2.1利用多处理器的处理能力
多处理器计算机系统曾经非常昂贵和稀有,只用在大型数据中心和科学计算基础设施中。今天它们更加便宜和丰富,即使是低端服务器和中档桌面计算机系统也往往拥有多个处理器。这种趋势只会增加,因为增加处理器的时钟频率(时钟频率:指同步电路中时钟的基础频率,是评定CPU的重要指标)越来越困难,处理器制造商将选择在一个处理器中包含更多的核心。所有的主流芯片制造商都已经开始了这种转变,并且我们已经目睹了一些拥有很多个处理器的计算机的出现。由于最基本的调度单元是线程,只拥有一个线程的程序一次最多只能在一个处理器上运行。在一个拥有两个处理器的计算机系统中,单线程程序放弃了一半的处理器资源。在一个拥有 100 个处理器的计算机系统中,单线程程序放弃了99%的处理器资源。另一方面,拥有多个线程的程序,同时可以在多个处理器中 执行。如果设计得比较合理,多线程程序可以更有效地利用处理器资源,从而增 加程序的吞吐量。 即使在单处理器计算机系统中,使用多线程也可以帮助提高吞吐量。如果一个程序是单线程的,在等待一个异步输入输出操作完成的时候,处理器处于空闲状态。如果该程序是多线程的,另一个线程就可以利用这个时间段来执行(这就类似于一边读报纸一边等待水沸,而不是等水沸了之后再读报纸)。
1.2.2简化建模
当你只有一种类型工作要做的时候,你更容易管理时间。如果你只有一种工作要做,你可以从第一件开始逐个完成,直到完成最后一件。你不必耗费精力来确定下一件要做的工作是什么。另一方面,管理多个不同优先权和截止期限的工作,并在不同类型的工作之间切换,需要一些额外的开销。对于软件来说也是如此,相比于同时管理多个不同类型任务的程序,一个管理一种类型任务的程序编写起来更简单,更不容易出错,更容易测试。一个复杂的异步工作流可以分解成多个简单的同步工作流,每个工作流在一个单独的线程中执行,只在特定的同步点进行线程间通信。一些框架(例如 Servlet 和 RMI)利用了多线程的这种好处。由框架来处理请求管理、线程创建、负载均衡等细节问题。Servlet 的编写者不需要担心同时 有多少其他请求被处理或者输入输出流是否阻塞等问题。当 Servlet 的 Service 方法被调用的时候,它可以将对该请求的响应当做一个单线程程序。这简化了 Servlet 组件的开发并削减了该框架的学习难度。
1.2.3简化异步事件的处理
一个服务器应用程序可以从多个远程客户端接受 Socket 连接,如果为每个连接都分配一个单独的线程,并使用阻塞式 I/O,这样的程序更容易开发。
如果一个程序从 Socket 中读取数据,但是 Socket 中没有数据,那么这个 read 方法就会阻塞,直到 Socket 中有数据可用。如果在单线程程序中,这不仅意味着相应的请求的处理被拖延,其他请求的处理也会被拖延。为了避免这个问题,单线程服务器程序被迫使用非阻塞式 I/O,相比于阻塞式 I/O 它更复杂也更容易出错。然而,如果每个请求都有自己的线程,那么一个线程的阻塞不会影响到其他请求。 由于历史原因,操作系统一般只允许一个进程拥有几百个或者更少的线程。操作系统提供了有效的基础设施(能力)来多路复用 I/O,例如 Unix 系统中的 select 和 poll 系统调用方法,Java 类库中提供了 java.nio 包来调用这些提供的能力。然而,操作系统开始逐渐支持更大数量的线程,使得为每个客户端分配一个线程的策略变为现实,即使是在拥有大量客户端的平台中。
1.2.4.提供给用户界面更好的响应能力
图形化用户界面(GUI)过去是单线程的,这意味着你必须经常轮询整个代码,查看是否有输入事件触发,或者间接地通过“主要事件循环”执行所有程序。如果主事件循环中调用代码的执行花了太多的时间,那么在这块代码返回之前,用户界面会显示出被“冻结”的状态。因 为在把运行的控制权返回主事件循环之前,无法处理接下来的用户界面事件。
如今的GUI框架,例如 AWT 和 Swing 工具箱,都使用事件分发线程来替换主事件循环。当一个用户界面事件比如一个单击按键事件触发的时候,在一个单独的事件线程中应用程序定义好的事件处理器会被调用。大多数GUI框架都是单线程子系统,所以作为父系统的主事件线程还在有效的工作,但是事件分发的线程却处于 GUI 工具箱的控制之下,而不是处于应用程序的控制之下。
如果在事件处理器的线程中只执行“短命”(原文这里是short-lived无引号)任务,用户界面就能合理快速的响应用户的操作行为。然而,如果在事件处理器的线程中执行一个耗时的任务,比如说对大文件进行拼写检查或者从网络上拉取资源,就会损害响应能力。如果用户在任务运行的过程中又做了个行为操作,在事件处理线程能够运行这个行为或者只是确认它之前会有一个很长的延迟。更糟糕的是,不仅UI界面没有响应,即使是界面上有取消按钮,你也无法取消这个正在执行的耗时的任务,因为此时事件分派线程正在忙碌,无法响应取消按钮的单击事件。但是如果耗时任务如果能分离出来开一个线程来执行的话,事件分派线程就能保持空闲状态去执行UI事件,这都保持了UI更好的响应能力。
1.3线程的风险
Java 提供的对多线程的内置支持是一把双刃剑。尽管它为多线程提供了语言和库的支持以及一个跨平台的内存模型(正是这个优秀的跨平台内存模型使我们能够开发一次,得到的java应用各个平台都能运行),简化了多线程应用程序的开发,但是它也对编程者提出了更高的要求,因为越来越多的程序将会使用多线程。多线程比较深奥,并发也随之成为了“高级”主题,现在,主流开发者必须清楚认识到线程安全的问题。
1.3.1.安全隐患
线程安全问题可能变得意想不到地微妙,因为在缺少合理的同步机制的情况下,多线程的执行顺序是不可预知的,有时甚至是令人惊讶的。下面的程序本来想产生一列无重复的整数值,在单线程环境中运行正常,在多线程环境中却运行失败。这个程序演示了多线程交错运行可能导致不符合我们需要的的结果。
上例在多线程环境中失败的原因是,在某些时候,两个线程调用 getNext() 方法的时刻非常接近,以至于返回的是同一个数值。value++看似是一个操作,实质上是三个操作:读取 value 值,value 值加 1,重设 value 值。由于多个线程中的操作在运行时可以任意地交错,可能出现两个线程同时获得的是相同数值的 value,然 后各自都加 1 的情况。结果就是在不同线程中的调用返回了相同的结果。
图1.1中图片所描述的就是不同线程交错处理。在这些图表中,时间从左到右依次运行,每一行代表一条不同的线程的所有活动,这些交错的图片描述出了最糟糕的案例,这个案例展现了在特定顺序下发生的错误设想结果的危险性。
UnsafeSequence使用了一个不标准的注解:@NotThreadSafe,这个注解是定义好的注解之一,这些注解在整本书中都有应用,用来记录类或者类成员的并发的属性。(别的类级别的注解还包括@ThreadSafe和@Immutable,可看备注A查看细节)。对于多数读者来说,线程安全文档注释是非常有用的。如果一个类被@ThreadSafe注释,使用者们可以在多线程环境下放心的使用,维护者们也能够注意到为线程安全提供必需的保证,软件分析工具也能鉴别代码里可能的错误。
UnsafeSequence这个类简单演示了一种常见的并发风险,叫做竞争条件。当被多个线程调用时,nextValue()方法是否返回一列无重复的整数值取决于多线程在运行时的交错情况,这种不确定性不是我们希望看到的。
由于多线程共享同样的内存空间,并且并发执行,它们可以访问并修改其他线程正在使用的变量。这样极其方便,因为相比于其他线程间通信机制这种方式使变量共享变得更简单。但共享变量也有极大的风险,数据可能会以不按我们要求进行的方式被修改掉,由于允许线程访问修改相同的变量,把非顺序性元素引入到顺序化的结构模型中,导致一些很难找出原因的 Bug。为了让多线程程序的行为具有可预测性,对共享变量的访问必须被合理地协调控制,这样能保证一个线程对该变量的访问不会干扰到另一个线程。幸运的是,Java 提供了同步机制来协调控制对共享变量的访问。
UnsafeSequence类里为 getNext()方法加上 synchronized 修饰符,解决不幸的冲突,上述代码可以被修复为如下代码:
如果没有synchronized关键字(比如将变量缓存在寄存器或者处理器本地缓存中、让它不被其他线程可见这样的功能),编译器、硬件以及运行时将拥有很大的自由来安排代码中操作执行的顺序和时机。这些优化技巧是为了帮助提高运行速度,但是给开发者增加了负担,他们必须明确地确认好多线程所共享的变量放置在哪里,这样才能保证这些优化手段不会破坏安全性。(第16章会给出JVM是如何确保顺序以及同步是如何影响的,但是要是你能采用章节2和章节3的规则,你就能安全的避开这些低级细节)
1.3.2.活跃性风险
在开发并发代码的时候一定要注意线程安全问题,线程安全性是不能妥协的。不仅多线程程序需要注意安全性,单线程程序也需要注意安全性和正确性,当然啦,多线程的使用确实引入了额外的安全风险。活跃性故障也会类似地随着多线程的使用产生,这在单线程程序中是不存在的。
如果说“安全”意味着“什么坏事都没发生”,活跃性关注的就是额外附加的目标--“最终会有一些好的事情发生”。当一个程序行为到达了永远无法继续运行前进下去的状态的时候,就意味着活跃性故障产生了。在单线程程序中有一种活跃性故障的形式,是由无限循环造成的,导致循环之后的代码永远无法被执行。多线程的引入也会增加了活跃性故障发生的可能性。例如线程A 在等待一个资源,该资源被线程 B 排他性地占有了,并且线程B永远不释放该资源,那么线程A必须永远等待下去,这就是一种活跃性故障。本书在第十章将会描述各种形式的活跃性故障,以及如何避免它们,包括死锁、饥饿、活锁。就像大多数并发 Bug 一样,由活跃性故障引起的 Bug 往往难以锁定,因为它们依赖于不同线程中事件发生的相对时机,因此在开发和测试过程中,这种 Bug会产生有时候有问题、有时候没问题的现象。
1.3.3. 性能风险
和活跃性相关的就是性能。活跃性意味着一些比较好的事情最终会发生--或者是不够好的结果最终发生了--不管怎么样我们都希望好的结构快点出现。性能问题包含一大堆其他问题,包括服务时间差、响应速度、吞吐量、资源消耗量和可伸缩性。就像安全性和活跃性问题一样,多线程程序不仅要面对单线程程序中所有的性能风险,还要面对由多线程引入的性能风险。
在设计良好的并发程序中,多线程的使用可以提高性能,但是多线程会增加运行时开销。在多线程程序中调度器经常需要临时挂起一个线程,运行另一个线程,这被称为上下文切换。上下文切换会造成很大的开销:保存和恢复执行上下文、本地损耗、调度线程替代运行的线程都需要时间。线程间的同步机制将会阻碍编译器对代码的优化、阻止内存缓冲区的清空和验证、在共享内存的总线上增加了所需要的同步流量,这些因素都会降低多线程程序的性能。第十一章将会介绍一些技术来分析并减少这些不利因素。
1.4. 多线程无处不在
即使你从没有显式地创建一个线程,框架也会代表你创建一些线程,并且这些线程调用的代码一定要是线程安全的。这对开发人员造成了很大的设计和实现负担。因为开发线程安全类相比于非线程安全类更困难,需要付出更多的精力。
每一个 Java 程序都是多线程的,因为当 JVM 启动的时候它创建了一些线程用来负责一些内部的工作(例如垃圾回收线程、释放资源)和 一个Main 线程。Main 线程用来执行 main 函数中的代码。AWT、Swing 等用户界面框架会创建一个线程用来管理用户界面事件。Timer 类会创建一个线程用来执行被延迟的任务。像Servlet或者 RMI框架会创建线程池来执行框架组件中的方法。
如果你像很多其他开发者那样使用这些框架的话,你必须对并发和线程安全比较熟悉,因为这些框架创建了多线程,并在这些线程中调用你开发的组件。理想情况是将多线程看做是 Java 的高级或者可选的特性,并不是必须掌握的,但是现实情况是,所有的 Java 程序都是多线程的,并且这些框架的使用并不能使你完全不用考虑多线程问题。
当你使用了一个带并发特性的框架(比如 Servlet)的时候,往往很难将并发限制于框架代码中,因为框架总是要使用回调来调用你编写的组件,从而来改变组件的内部状态。类似地,对线程安全性考虑不仅仅对是在框架调用组件后结束,在组件修改程序状态的过程中但凡涉及到的代码运行路径上的类都要考虑线程安全性。因此,线程安全性需求是有传播性质的。
框架的线程调用应用程序中的各个组成部分会把并发引入到整个应用中。组件总是访问应用程序的状态,因此需要所有代码能够保证状态这个参数一定是线程安全的。
下面介绍的所有功能都会导致你编写的程序中的一些代码在其他的线程中被调用,这些线程并不是由应用程序管理的。尽管线程安全性需求起源于这些功能,但却不会因为功能停止而终止需求,这些需求将会像水面涟漪一般扩散至整个应用中。
Timer 类:
Timer 类是一个方便的机制,用来一次性或周期性调用定时任务。Timer 类的引入会使得原本序列化的程序变得复杂,因为TimerTask 是在 Timer 管理的某个线程中执行的,而不是在应用程序所管理的。如果某个TimerTask访问了一个被其他线程访问的数据,两者都必须以线程安全的形式进行访问。通常来说最简单的办法就是让被 TimerTask 访问的对象本身就是线程安全的,也就是说将线程安全性封装在共享对象中。l
Servlet 和 JSP:
Servlet 框架用来部署 Web 应用程序并将远程 HTTP 请求分发到不同的线程中执行。一个请求在到达服务器后将会被分发,然后有可能会通过一组过滤器,最终到达合适的Servlet或者JSP。每个Servlet就是一个应用逻辑组件,在高访问量的服务器中很有可能出现多个客户端同时访问同一个Servlet的情况。Servlet规格要求说明中要求Servlet能够同时被多线程调用,换句话说,必须是线程安全的。
即使在一个Web应用程序中,你可以确定某个时刻Servlet同时只会在一个线程中运行,你还是要注意线程安全问题。Servlet总是允许和其他Servlet共享状态信息,例如存储在 ServletContext 中的应用程序域对象或者存储在HttpSession中的session域对象。如果说一个servlet允许和其他servlet或者请求共享对象,那他就需要确保当不同的线程同时访问这些对象的时候,能够恰当的协调好对这些对象的访问。这些共享对象(Servlets、JSPs、servlet过滤器、对象等)也需要存储在能够确保线程安全的诸如ServletContext、Httpsession的域容器中。 l
RMI:
RMI允许你调用另一个 JVM 中对象的方法,当你使用 RMI 调用远程方法的时候,方法参数会被打包成字节流,通过网络传送到远程 JVM 中,然后被还原为对象或原始数据类型,传送给远程方法。
当远程对象的方法被调用的时候,是在哪个线程中调用的?你可能并不清楚,但他一定不是你创建的线程--而是在一个由RMI管理的线程被调用。同一个远程对象的同一个远程方法会被多个RMI线程同时调用吗?
远程对象必须防范两个线程安全问题:被多个远程对象共享的对象应该是线程安全的,远程对象本身应该是线程安全的。像servlets、RMI对象都要能保证自己在被多个线程调用时是线程安全的。 l
Swing 和 AWT:
GUI 应用程序与生俱来就是异步的。用户可能在任何时候选择一个菜单项或者按下一个按钮,并且用户希望应用程序即便是在执行任何任务的过程中都能够立即响应用户界面操作。Swing 和 AWT 通过创建一个独立的用于处理用户界面事件和更新图形界面的线程来解决这个问题。
Swing 组件,例如 JTable 不是线程安全的。替代的手段是,Swing 程序通过将所有对 GUI 的访问限制在事件响应线程中来达到线程安全的目的。如果一个程序想要在事件响应线程之外操作 GUI 的话,必须让操作 GUI 的代码在事件响应线程中执行。
当用户在 GUI 上进行了某个操作,事件响应线程中某个事件处理函数被调用来执行用户的需求。如果这个事件处理函数需要访问被其他线程共享的对象,那么事件处理函数和其他一起共用的对象那些线程都必须考虑线程安全问题。