6 第1章介绍 清单1.1非线程安全的序列生成器 @NotThreadSafe public class UnsafeSequence private int value; 60 /*★返回一个唯一值.*/ public int getNext(){ return value++; A value→9 9+1→10 value=10 B value→9 9+1→10 value=10 图1.1 UnsafeSequence.getNext执行失败 UnsafeSequence中的问题是,在一些特殊的时序情况下,两个线程可以调用getNext 并得到相同的返回值。图1.1表现了这是如何发生的。自增操作va1ue++可能看起来是一 个单一的操作,但是事实上它分为3个独立的操作:读取这个值,使之加1,再写入新值。 因为这些操作发生在多个线程中,这些线程可能交替占有运行时(runtime),所以两个线 程很可能同时读取这个值,两个线程都得到相同的值,并都使之增加了1。结果就是不同 的线程返回了相同的序列数。 图1.1中的图表描述了不同线程之间的交替操作。在这些图表中,时间由左至 右发展,每一行表现一个不同线程的活动。这些交替的图表通常用来描述最坏的情 况2,目的是表现特定顺序下产生错误的僭越带来的危险。 UnsafeSequence 使用了一个非标准的标签(Annotation):@NotThreadSafe。这是 本书中几个自定义的Annotation之一,这些Annotation用来标明类和类成员的并发特 2事实上,像我们将在第3章看到的那样,最坏的情况可能比图中表现的更糟糕,因为存在重排 序的可能性。 Java并发编程实践
1.3线程的风险 性。(其他的类级Annotation还有:@Threadsafe和@Immutable;详见附录A)用Annotation 给线程安全进行标记对于各类读者来说是非常有用的。如果一个类标记为@Threadsafe, 用户就可以充满信心地把它应用于多线程环境,维护者看到它可以认为是线程安全的必然 保证,而软件分析工具可以转而去识别那些可能存在的代码错误。 UnsafeSequence阐明了一种常见的并发危险:竞争条件(race condition)。当被多线 程调用时,getNext是否能返回不重复的值,正像它的规约描述的,而这取决于运行时如 何交替进行这些操作一这不是我们所希望看到的势态。 因为线程共享相同的内存地址空间,且并发地运行,它们可能访问或修改其他线程正 在使用的变量。这是十分方便的,因为它使得数据共享相对于其他的线程间通信机制都更 加简单。但是这其中也存在着巨大的风险:当数据意外改变时,线程可能会出现混乱。允 许多线程访问和修改相同的变量,给顺序编程模型引入了一些非顺序因素,这可能会造成 混乱,并且难以发现错误的原因。为了使多线程程序的行为可预见,访问共享的变量必须 经过合理的协调,这样线程才不会相互干扰。幸运的是,Java提供了同步机制来协调这样 的访问。 像清单1.2中那样,可以通过把getNext声明为synchronized类型的方法来修正 UnsafeSequence,因此可以避免图l.l所示的那种不应出现的交互。(这样做能够避免这 个错误的确切原因将是第2章和第3章的主题。) 清单1.2线程安全的序列生成器 @ThreadSafe public class Sequence @GuardedBy("this")private int value; public synchronized int getNext()( return value++; 在缺少同步的时候,编译器、硬件和运行时事实上对时间和活动(action)顺序是很 随意的,比如在寄存器或者高速缓存中存储变量,这样会使它们对于其他线程暂时(甚至 是永远)不可见。这样的行为被普遍认为是能提高性能的非常可行的办法,但是这给开发 人员带来了负担,他们需要明确识别数据在线程中究竟如何共享,这样这些优化才不至于 破坏安全。(第16章给出了一些非常详细的细节,VM确切地产生了什么样的顺序担保, 3@GuardedBy在2.4小节会进行描述。它文档化了顺序序列的同步策略。 Java并发编程实践
8 第1章介绍 同步如何影响这些担保的,不过只要你遵循第2章、第3章中的规则,那么避开这些底层 的细节也能达到安全性。) 1.3.2活跃度的危险 在开发并发代码时,对线程安全的关注是至关重要的:安全不能妥协。安全的重要性 不仅仅存在于多线程程序中,单线程化的程序也必须注意保护安全性和正确性,但是线程 的使用引入了不会出现在单线程化程序中的额外安全危险。举例来说,线程的使用引入了 又一形式的活跃度失败(liveness failure),这不会出现在单线程化的程序中。 如果安全意味着“什么坏事都没有发生过”,活跃度关注是与之互补的一面“好事最 终发生了”。当一个活动进入某种它永远无法再继续执行的状态时,活跃度失败就发生了。 一种活跃度失败可以发生在顺序程序中,这就是粗心造成的无限循环,那些在循环之后的 代码永远不会被执行。多线程的引入带来了更多的活跃度危险。例如,如果线程A等待一 个线程B独立占有的资源,B永远不释放这个资源,A将永远等待下去。第10章将讲述 各种形式的活跃度失败,包括死锁(deadlock,l0.1小节)、饥饿(starvation,10.3.1小 节)、活锁(1 ivelock10.3.3小节),以及如何避免它们发生。像大多数同步bug一样,引 起活跃度失败的g总是难以察觉到,因为它们取决于线程间的相关的事件时序,因此在 开发和测试中,并没有很多机会发现它们。 1.3.3性能危险 与活跃度相关的是性能(performance)。虽然活跃度意味着好的事情终究会发生,但 是最后可能还是不够好一我们通常希望好事情尽快发生。性能问题涉及很多方面,包括 服务时间、响应性、吞吐量、资源消费或者可伸缩性的不良表现。就像安全和活跃度一样, 多线程程序出现所有单线程程序中遇到的性能危险,而且还会有因线程的使用带来的风 险。 在设计良好的应用程序中使用线程,能够获得纯粹的性能收益,但是线程仍然会给运 行时带来一定程度的开销。上下文切换(Context switches)一当调度程序临时挂起当 前运行的线程时,另一个线程开始运行一这在多个线程组成的应用程序中是很频繁的, 并且带来巨大的系统开销:保存和恢复线程执行的上下文,离开执行现场,并且CPU的 时间会花费在对线程的调度而不是在运行上。当线程共享数据的时候,它们必须使用同步 机制,这个机制会限制编译器的优化,能够清空或锁定内存和高速缓存,并在共享内存的 总线上创建同步通信。所有这些因素又引入了新的性能开销;第11章将介绍分析和降低 这些开销的技术。 Java并发编程实践
1.4线程无处不在 9 1.4线程无处不在 即使你的程序没有显式地创建任何线程,框架也可能为你创建了一些线程,这些线程 调用的代码必须是线程安全的(thread-safe)。这一点给开发人员的设计和实现赋予了更 重要的一份责任,因为开发线程安全的类要比非线程安全的类需要更加仔细,进行更多的 分析。 每一个Java应用程序都使用线程。当JVM启动后,它创建一些线程来进行自身的常 规管理(垃圾回收,终结处理),以及一个运行main函数的主线程。AWT和Swing用户 接口框架创建线程来管理用户接口事件。Timer创建执行延迟的任务线程。组件框架,比 如serv1et和RMI,会创建线程池,池中线程调用组件方法。 如果你使用这些功能一像大多数程序员一样—你必须熟悉并发和线程安全,因为 这些框架创建线程,并在线程中调用你的组件。如果你愿意自欺欺人地认为并发是“可选 的”、“高级的”语言特性,这很好。不过事实上,几乎所有的Java应用程序都是多线程 的,并且这些框架也不能让你完全避开对应用程序状态访问作适当的协调。 当并发由一个框架引入到一个应用程序中,并不能仅仅让框架的代码知晓并发的存 在。因为框架代码本质上是通过回调来使用程序组件,而这些组件才真正用来访问程序状 态。与之类似的是,线程安全的需要并不仅仅在于框架调用的组件一只要它处于组件访 问过的程序状态段,它就会扩展到所有代码路径。因此,线程安全的需要是具有传递性的。 通过从框架线程中调用应用程序的组件,框架把并发引入了应用程序。组件总 是需要访问程序的状态。因此要求在所有代码路径访问状态时,必须是线程安全的。 以下描述的这些场景,都会引发非应用程序管理线程调用应用程序代码这种情况。当 需要线程安全的时候,可能会以这样的情况作为开始,可是这样做几乎都不能正常结束: 反而会影响到整个程序。 定时器。Timer用来调度一些稍后运行的任务,也可以是只运行一次或者周期性运行 的任务,这是一种非常方便的机制。引入Timer可以使一个通常简单的顺序程序变复杂, 因为TimerTasks运行在由Timer管理的线程中,并不是由应用程序来管理。如果一个 TimerTask访问了其他应用程序线程正在访问的数据,那么不仅TimerTask需要线程安 全的手段,并且其他那些同时访问这个数据的类也需要相应措施。通常最简单的实现方法 Java并发编程实践
10 第1章介绍 是确保TimerTask访问的对象本身是线程安全的,因此应该将线程安全性封装到共享对 象的内部。 Servlets and JavaServer Pages(JSPs)Servlets框架的设计目的是处理Web应用的部 署,分发来自远程HTTP客户的请求这些的基础层业务。一个请求到达Server并被分发后, 可能通过一个过滤器链到达相应的Servlet或者JSP。每一个Servlet代表应用逻辑的一个 组件,在访问量较大的网站中,许多客户可能同时对相同Servlet的服务提出请求。Servlets 的规范规定了一个Servlet必须为多个用户同时调用它作好准备。换句话说,Servlets应该 是线程安全的。 即使你能够保证一个Servlet一次只被一个线程调用,你在建立一个Wb应用程序时 可能仍然需要注意线程安全。Servlets通常访问与其他Servlets共享的状态信息,比如程序 范围内的对象(那些存储于ServletContext的对象)或者Session范围内的对象(这些 保存在每个客户的HttpSession中)。当一个Servlet访问的对象是在Servlets间共享或 者请求间共享时,必须对这些对象的访问控制进行适当协调,因为来自不同线程的多个请 求可能同时访问它们。Servlet、.JSP和Servlet Filter以及那些存储在ServletContext和 HttpSession容器中的对象,明显必须是线程安全的。 远程方法调用(Remote Method Invocation)RMI使你能够调用在另外一个JVM上运 行的对象的方法。当你使用RⅫ调用一个远程方法时,这个方法的参数被打包(装配) 成一个比特流,并且穿越整个网络到达远程VM,在那里它会被解包(分解)并传递给远 程方法。 当RMI代码调用了你的远程对象时,这个调用发生在哪一个线程?你并不知道,但 绝对不是你创建的那个线程一你的对象被RMI管理的一个线程所调用。RMI创建了多 少个线程?在许多RMI线程中,同一个对象的同一个方法是不是有可能同时被调用4? 一个远程对象必须去守卫两种线程安全风险:对那些可能会与其他对象共享的状态进 行适当调节,应正确地对远程对象本身进行调控(因为相同的对象可能同时被多个线程调 用)。比如servlets,RMI对象应该对同时发生的多个调用有所准备,并且必须提供它们 自己的线程安全。 Swig和AWT GUI应用程序具有固有的异步特性。用户可能选择一个菜单项,或 者在任何时候按下一个按钮,他们希望程序迅速作出响应,即使程序正在做其他事情。 Swing和AWT通过创建一个单独的线程来处理用户发起的事件,并更新那些展现给用户 的图形界面。 4答聚是:是的。但是Javadoc.里面写的并不是很详细一你必须去阅读RMI规范. Java并发编程实践