Java并发编程实战第二章 线程安全
编写线程安全代码的核心是管理对状态的访问,尤其共享和可变状态的访问。通过同步协调对对象可变状态的访问可以实现一个对象的线程安全。同步最基本的机制是synchronized
关键词,但是同步(synchronization)这个属于还包括:volatile
变量、显式(explicit)锁和原子(atomic)变量。
最佳实践
- 在设计时就考虑线程安全
- 利用封装(encapsulation),不可变性(immutablility),清晰指定不可变量(invariants)是比较好的实践。
2.1 什么是线程安全
- 所有线程安全合理定义的核心都是正确性。正确性 意味着类符合其规范,一个良好的规范定义了约束对象状态的不变量和描述操作结果的后置条件。
线程安全的定义: 如果一个类在多个线程访问时正确运行,则它是线程安全的,不管运行时环境如何调度或者交错这些线程的执行,并且在调用代码部分没有多余的同步和协调。
- 线程安全类会封装任何需要的同步,从而使客户不必在自己实现。
- 无状态(
stateless
)的类永远是线程安全的
2.2 原子性
2.2.1 Race Condition
一个竞争条件会在计算正确性依赖于运行时中多线程相对时间或者交错出现。换句话说,正确答案取决于幸运时机。大部分的竞争条件的类型为
check-then-act
型,此时可能会有陈旧观察用于决定下一步做什么。
例子:
- 自增操作是一个
read-modify-write
操作,分三步完成而非一步,所以是非原子操作。 - 星巴克约会的例子
lazy initialization
的例子
原子操作
如果从一个执行A复合操作的线程的角度来看,执行B复合操作的线程要么将B所有操作执行完,要么完全不执行B系列的任何操作,那么,A和B是对彼此来说是原子的。一个原子操作是指与包括自己在内的所有在同一状态上的操作集都是互为原子操作的操作集。
原子变量
在java.util.concurrent.atomic
包下有很多原子变量,可以执行原子操作。如AtomicLong
的实例方法incrementAndGet()
。对于无状态的类添加一个线程安全的属性,则这个类仍然是线程安全的,但是如果添加的多余一个,则不保证这个类依然是线程安全的类。
在实践中,要尽量使用线程安全对象来管理类的状态。
2.3 加锁
Intrisic Lock(固有锁)
synchronized
是Java内置固有锁,即可以修饰方法,又可以修饰代码块。修饰方法时默认由方法所属对象作为intrisic locks
或者monitor locks
可重入性(Reentrancy)
- 可重入意味着锁被以线程而非以调用为单位获取。
- 可重入通过把锁与获取计数器和其所属线程关联来实现:计数器为0时锁是unheld状态的,当一个线程获取一个unheld状态的锁后,计数器加1,如果此线程的再一次(如调用了另一个相同锁的方法)调用,则计数器加1,而非被阻塞;当线程离开
synchronized
块之后,计数器减1.
2.4 使用锁保护状态
- 每个共享和可变的状态变量都应该被一个且仅一个锁保护,从而使维护人员知道是哪一个锁。
- 给一个对象内的所有可变量加锁是一个惯例。但是这个加锁协议很容易因为添加一个新的方法或者
code path
被破坏。比如一个单线程程序引入一个异步的TimerTask
- 对每个包含多个变量的不变性条件(
invariants
),其中涉及的所有变量都需要由同一个锁保护。 - 即使对象中的所有方法都是原子的,也不能保证复合操作是原子的,还需要额外的加锁机制。此外每个方法都作为同步方法还容易引起活跃性(Liveness) 问题和 性能问题。
2.5 活跃性和性能问题
- 应该在保证原子性的情况下缩小同步的范围。
- 占有锁的时间过长可能会导致活跃性问题和性能问题
- 避免在长时计算或者不能快速结束如网络或者控制台IO等操作时保持锁。