书籍:《java concurrency in practice》英文原版
作者:Brian Goetz
2.1
线程安全如何定义?
首先,“线程安全”针对的对象是java的类。通常是描述“某个类是否线程安全”。
其次,一个类是“线程安全”,意味着它不需要使用任何“同步”、“协调”的代码就能在多个线程内正常运行
由此可知,只有“非线程安全”的类,才需要借助“同步”工具进行辅助
我们很难理解线程安全的概念,是由于缺乏一个清晰精准的定义,而“精准”意味着class必须要遵循它的规范。一个好的规范定义了约束对象状态的不变量(invariants)和描述其操作效果的后置条件(postconditions)。
说人话就是:书中给与”线程安全”的定义是:一个类是线程安全的话,那它必须保证在多线程操作下,它的invariants保持不变,而且操作结果符合给定的条件。
举个例子,”因素分解servlet缓存”例子中,请求入参的lastNumber和计算出来的lastFactors,组成了一个不变量(invariant),就是无论如何操作,用lastNumber在缓存取得的值一定与计算出来的lastFactors的一样。此处可见一个invariant可由多个相关联的state组成
2.1.1
无状态的类总是线程安全的。
2.2
通过了一个”counter++”的例子,解释了什么叫有状态,且有状态是线程不安全的。因为“counter”的操作包含了”读、改、写“三步,在多线程中这种操作并不是原子的。这种情况叫做”竞态(race condition)“
2.2.1
通过了”两人相约星巴克”的例子,展示了竞态的问题。简单来说就是:你观察某个条件为ture,当你基于这个观测来行动的时候,在观测到行动的这段时间内系统的状态已经发生了改变。这种竞态问题叫做“检查后行动(check-then-act)”
2.2.2
通过“懒初始化(lazy initialization)”例子来展示了代码层面的“检查后行动”问题。同时揭示了“counter”问题属于另一种竞态问题:“读、改、写(Read-modify-write)”。还举了些例子也会有这种问题,诸如:注册器、id生成器
2.2.3
把”检查后行动“和”读、改、写“这两种竞态问题概括为”复合操作(compound actions)”。而“复合操作”必须保证原子性。介绍了现成的”线程安全类”:Atomic*,用于解决“读改写”问题。同时作者建议多使用现成的“线程安全对象“来解决线程安全问题,而不是一上来就加锁。
2.3
通过了”对servlet的请求处理增加缓存”的例子,揭示了当程序有多个状态时,我们对每个状态都使用”线程安全对象”来维护,也是没法保证线程安全性的。因为这多个状态是相互关注的,为了保护状态的一致性,应当在原子性操作里更新相关联的变量。
2.3.1
介绍了保证多个状态原子性的手段:内置锁 synchronized,又称 intrinsic locks,monitor locks。
介绍了注解:@GuardedBy(“this”),可以标注变量由哪个锁保护,仅用于提高可读性。
同时注明了过度的”线程安全”则会造成”性能问题”,
2.3.2
介绍了什么叫可重入
2.3.4
当可变状态被多个线程访问时,要对所有的访问动作都加同一个锁,此时可以说这个状态被那个锁保护着。
每个可变的、共享的状态都应该只由一个锁来保护,我们应该让维护者明确的知道保护状态的是哪个锁。
一个常见的做法是,当一个对象封装了多个可变状态,这些状态通常会用封装它们的对象的内置锁来保护。
作者再次强调,只有可能被多线程访问的可变对象,才需要使用锁来保护。
一个invariant设计的多个state,应该由同一把锁来保护
总的来说,线程安全围绕着三种解决思路,按顺序优先使用:
- 要么不共享
- 要么发布不可变对象,对此有三个要求
- 对象状态不可变(指操作)
- 对象里的引用需要使用final修饰
- 正确的构造(不暴露this)
- 最后他自己是要可见的(添加volatile修饰)
- 要么使用同步机制(synchronized 、 lock)
2.4
最后还强调了并不是对每个方法都加锁就能解决竞态问题,因为有些需要原子性的复合操作需要涉及到多个方法,而且这样做反而有可能造成活跃性问题和性能问题。
2.5
作者说明了对整个方法加锁不是一种好的做法,会直接导致所有线程都排队执行该业务,称其为“弱并发”。并提供了一种思路,就是尽量把不影响”共享状态”的耗时操作移出同步块,让这些耗时操作可以利用到并发的优势。这个思路能让我们在简单性、安全性和并发性之间找到一个平衡。如何找到这种平衡,衡量同步块的规模,要求要有tradeoffs思维。有时候简单性和性能是互相矛盾的,在实现同步的时候要注意抵制”过早牺牲简单性来满足性能”的诱惑。要避免在漫长的计算过程中持有锁。