1.4线程无处不在 11 Swing组件,比如JTable,都不是线程安全的。相反,Swing程序通过限制访问事件 线程中的GUⅡ组件,实现了它们的线程安全。如果应用程序希望从事件线程之外进行操控, 它必须促使操控GUI的代码在事件线程中运行。 当用户进行了一个UI活动,事件线程会调用事件处理器(handler)来执行用户请 求的操作。如果处理器需要访问应用程序状态,这里很可能有其他程序在正在访问(比 如正在编辑的文件),那么事件的处理器,与其他访问状态的代码必须以线程安全的方 式工作。 Java并发偏程实践
八
PART 第1部分 基础 Fundamentals Java并发编程实践
CHAPTER 第2章 线程安全 Thread Safety 也许你会惊讶,并发编程并不会涉及过多的线程或锁,不会多于建筑工程中使用的铆 钉和I型梁。当然,要让桥梁坚固耐用,需要正确使用大量的铆钉和I型梁;同样的道理, 构建并发程序也要正确使用线程和锁。然而这仅仅是纸上谈兵(mechanisms)一获得最 终结果的方式。编写线程安全的代码,本质上就是管理对状态(state)的访问,而且通常 都是共享的、可变的状态。 通俗地说,一个对象的状态就是它的数据,存储在状态变量(state variables)中,比 如实例域或静态域。对象的状态还包括了其他附属对象的域。例如,HashMap的状态一部 分存储到对象本身中,但同时也存储到很多Map.nty对象中。一个对象的状态包含了 任何会对它外部可见行为产生影响的数据。 所谓共享,是指一个变量可以被多个线程访问:所谓可变,是指变量的值在其生命周 期内可以改变。我们讨论的线程安全性好像是关于代码的,但是我们真正要做的,是在不 可控制的并发访问中保护数据。 个对象是否应该是线程安全的取决于它是否会被多个线程访问。线程安全的这个性 质,取决于程序中如何使用对象,而不是对象完成了什么。保证对象的线程安全性需要使 用同步来协调对其可变状态的访问:若是做不到这一点,就会导致脏数据和其他不可预期 的后果。 无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该 变量,此时必须使用同步来协调线程对该变量的访问。Java中首要的同步机制是 synchronized关键字,它提供了独占锁。除此之外,术语“同步”还包括volatile变 量,显示锁和原子变量的使用。 你会想到在一些“特殊”情况下上述规则并不适用,不过你应该抵制住这种想法的诱 惑。程序如果忽略了必要的同步,可能看上去可以运行,而且能够通过测试,甚至能正常 地运行数年,但它仍然是存在隐患的,任何时刻都有可能崩溃。 Java并发编程实践
16 第2章 线程安全 在没有正确同步的情况下,如果多个线程访问了同一个变量,你的程序就存在 隐患。有3种方法修复它: ●不要跨线程共享变量; ●使状态变量为不可变的;或者 ●在任何访问状态变量的时候使用同步。 如果你没有在类的设计中考虑并发访问的因素,需要使用上面的3种方法对类的设计 作重大的修改,所以修复程序的问题并不像听上去那样轻而易举。一开始就将一个类设计 成是线程安全的,比在后期重新修复它更容易。 在一个大型的程序中,识别出是否有多个线程可能访问给定的变量并不是一件容易的 事情。幸运的是,面向对象技术—比如封装和数据隐藏一不仅帮助你编写组织良好的、 可维护的类,同样的技术还可以帮助你创建线程安全的类。访问特定变量的代码越少,越 容易确保使用恰当的同步,也越容易推断访问一个变量所需的条件。Java语言不强迫你封 装所有的域,允许你将状态存储到公共域(甚至是公共静态域),或者将它的引用发布到 其他的内部对象中,对程序的状态封装得越好,你的程序就越容易实现线程安全,同时有 助于维护者保持这种线程安全性。 设计线程安全的类时,优秀的面向对象技术一封装、不可变性以及明确的不 变约束——会给你提供诸多的帮助。 很多时候,良好的面向对象设计技术与现实世界需求不匹配:这种情况下,出于对系 统性能和遗留代码的向后兼容性的考虑,良好的设计规则必须向现实世界作出妥协。有时 候,抽象和封装会与性能产生冲突,虽然不像很多开发者认为的那样频繁,但是,首先让 你的代码正确,然后(thn)再让它跑得快,总是一个良好的实践。即便如此,性能优化 也只发生在特定的条件下:性能标准和需求要求你必须去做,或者依据相同的衡量标准, 你的优化运行在真实环境中时发生了变化'。 如果你决定必须打破封装也无所谓。你的程序仍然可以是线程安全的,但是实现起来 1对于并发代码,要更加坚定地遵循这个实践。因为并发性错误很难再现与调试,从一些不常 用的代码路径上所获得的些许性能上的提升,与可能给程序带来的失败风险比起来,就得不偿 失了。 Java并发编程实践