什么是线程安全性

也许你会惊讶,并发编程并不会涉及过多的线程或锁,不会多于建筑工程中使用的铆钉和I型梁。当然,要让桥梁坚固耐用,需要正确使用大量的铆钉和I型梁;同样的道理,构建并发程序也要正确使用线程和锁。然而这仅仅是纸上谈兵(mechanisms)——获得最终结果的方式。编写线程安全的代码,本质上就是管理对状态(state)的访问,而且通常都是共享的、可变的状态。
通俗地说,一个对象的状态就是它的数据,存储在状态变量(state variables)中,比如实例域或静态域。对象的状态还包括了其他附属对象的域。例如,HashMap的状态一部分存储到对象本身中,但同时也存储到很多Map.Entry对象中。一个对象的状态包含了任何会对它外部可见行为产生影响的数据。
所谓共享,是指一个变量可以被多个线程访问;所谓可变,是指变量的值在其生命周期内可以改变。我们讨论的线程安全性好像是关于代码的,但是我们真正要做的,是在不可控制的并发访问中保护数据。
一个对象是否应该是线程安全的取决于它是否会被多个线程访问。线程安全的这个性质,取决于程序中如何使用对象,而不是对象完成了什么。保证对象的线程安全性需要使用同步来协调对其可变状态的访问;若是做不到这一点,就会导致脏数据和其他不可预期的后果。
无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。Java中首要的同步机制是synchronized关键字,它提供了独占锁。除此之外,术语“同步”还包括volatile变量,显示锁和原子变量的使用。
你会想到在一些“特殊”情况下上述规则并不适用,不过你应该抵制住这种想法的诱惑。程序如果忽略了必要的同步,可能看上去可以运行,而且能够通过测试,甚至能正常地运行数年,但它仍然是存在隐患的,任何时刻都有可能崩溃。
在没有正确同步的情况下
如果多个线程访问了同一个变量,你的程序就存在隐患。有3种方法修复它:
l 不要跨线程共享变量;
l 使状态变量为不可变的;或者
l 在任何访问状态变量的时候使用同步。
如果你没有在类的设计中考虑并发访问的因素,需要使用上面的3种方法对类的设计作重大的修改,所以修复程序的问题并不像听上去那样轻而易举。一开始就将一个类设计成是线程安全的,比在后期重新修复它更容易。
在一个大型的程序中,识别出是否有多个线程可能访问给定的变量并不是一件容易的事情。幸运的是,面向对象技术——比如封装和数据隐藏——不仅帮助你编写组织良好的、可维护的类,同样的技术还可以帮助你创建线程安全的类。访问特定变量的代码越少,越容易确保使用恰当的同步,也越容易推断访问一个变量所需的条件。Java语言不强迫你封装所有的域,允许你将状态存储到公共域(甚至是公共静态域),或者将它的引用发布到其他的内部对象中,对程序的状态封装得越好,你的程序就越容易实现线程安全,同时有助于维护者保持这种线程安全性。
设计线程安全的类时,优秀的面向对象技术——封装、不可变性以及明确的不变约束——会给你提供诸多的帮助。
很多时候,良好的面向对象设计技术与现实世界需求不匹配;这种情况下,出于对系统性能和遗留代码的向后兼容性的考虑,良好的设计规则必须向现实世界作出妥协。有时候,抽象和封装会与性能产生冲突,虽然不像很多开发者认为的那样频繁,但是,首先让你的代码正确,然后(then)再让它跑得快,总是一个良好的实践。即便如此,性能优化也只发生在特定的条件下:性能标准和需求要求你必须去做,或者依据相同的衡量标准,你的优化运行在真实环境中时发生了变化1。
如果你决定必须打破封装也无所谓。你的程序仍然可以是线程安全的,但是实现起来
更困难些。而且,程序的线程安全性会变得更加脆弱,这增加了开发与维护的开销和风险。第4章详述了在什么条件下你可以安全地打破对状态变量的封装。
到目前为止,我们几乎可以互换地使用着术语“线程安全类”和“线程安全程序”。一个线程安全程序是完全由线程安全类构成的么?不必要——完全由线程安全类构成的程序未必是线程安全的,线程安全程序也可以包含非线程安全的类。围绕着组合线程安全类的话题,还会在第4章提到。无论如何,只有当类封装了自己的状态时,“线程安全类”的概念才有意义。“线程安全性”可能成为用于约束代码的条款,或成为状态的条款,并且只能用于封装了自身状态的代码的整体,这个整体可能是一个对象,或是一个完整的程序。
2.1  什么是线程安全性
给“线程安全性”下个定义相当棘手。很多正式的定义都显得过于复杂,并没有给出实用的指导或者精到的见解;而其他非正式的描述看上去又完全是在兜圈子。在Google上搜索了一下,查到很多定义略举一二:
…可以被多个程序线程调用,这些线程之间没有非预期的互交。
…可以同时被多个线程调用,而调用者不需要任何动作(来确保线程的安全性)。
给出这样的定义,让我们对线程安全性产生困惑是不足为奇的!它们听上去令人怀疑:“如果一个类可以安全地被多个线程使用,它就是线程安全的。”你无法对此论述提出任何争议,但也无法从中得到更多有意义的帮助。我们如何辨别线程安全与非线程安全的类?我们甚至又该如何理解“安全”呢?
任何一个合理的“线程安全性”定义,其关键在于“正确性”的概念。如果我们关于线程安全性的定义是模糊的,那是因为缺少一个明确的“正确性”定义。
正确性意味着一个类与它的规约保持一致。良好的规约定义了用于强制对象状态的不变约束(invariants)以及描述操作影响的后验条件(postconditions)。通常我们不会为类写足够的规约,那么我们还能够知道程序的正确与否么?不能,但是只要我们相信“代码是可以工作的”,就不会阻止我们使用这些类。这种“代码自信”与我们所要实现的正确性紧密相关,所以不妨假设单线程化的正确性是“所见即所知”的事物。乐观地将“正确性”定义为“可被认知事物”后,我们现在可以少兜些圈子来定义“线程安全性”了:一个类是线程安全的,是指在被多个线程访问时,类可以持续进行正确的行为。
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。
任何单线程化的程序同时也是合法的多线程化的程序,倘若程序在单线程化的环境尚且不正确2,那么该程序必然不是线程安全的。对于一个正确实现的对象,顺序性的操作——比如调用公共的方法,读写公共域——不会破坏任何一个不变约束以及后验条件。对于线程安全类的实例进行顺序或并发的一系列操作,都不会导致实例处于无效状态。
线程安全的类封装了任何必要的同步,因此客户不需要自己提供。
2.1.1  示例:一个无状态(stateless)的servlet
在第1章,我们列出了很多框架,这些框架会创建线程,并在这些线程中调用你的组件,而将确保组件线程安全的责任留给了你。通常需要线程安全的,并不是直接使用线程的情况,而是那些使用了便利工具(如Servlets框架)的情况。我们会展示一个例子,基于Servlet的因数分解服务,并逐步扩展它,添加新特性,同时确保它的线程安全性。
清单2.1展示了我们简单的因数分解的Servlet。它从Servlet Request中解包数据,然后将这个数据进行因数分解,最后将结果封包到Servlet Response中。
清单2.1  一个无状态的Servlet
@ThreadSafe
public class StatelessFactorizer implements Servlet {
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(resp, factors);
    }
}
StatelessFactorizer像大多数Servlet一样,是无状态的:它不包含域也没有引用其他类的域。一次特定计算的瞬时状态,会唯一地存在本地变量中,这些本地变量存储在线程的栈中,只有执行线程才能访问。一个访问StatelessFactorizer的线程,不会影响访问同一个Servlect的其他线程的计算结果;因为两个线程不共享状态,它们如同在访问不同的实例。因为线程访问无状态对象的行为,不会影响其他线程访问该对象时的正确性,所以无状态对象是线程安全的。
无状态对象永远是线程安全的。
多数Servlet都可以实现为无状态的,这一事实极大地降低了确保Servlet线程安全的负担,只有当Servlet要为不同的请求记录一些信息时,才会将线程安全的需求提到日程上来。
 
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值