第二章 线程安全性
如果当多个线程访问同一个可变的状态变量是没有使用合适的同步,那么程序就会出现错误。有三种方法可以修复这个问题:
- 不在线程之间共享该状态变量。
- 将状态变量修改成不可变的变量。
- 在访问状态变量时使用同步。
什么是线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确行为,那么这个类就是线程安全的。
对象的状态是指存储在状态变量中的数据。对象的状态可能包括其他依赖对象的域。无状态:它既不包括任何域,也不包括任何对其他类中域的引用。无状态对象一定是线程安全的。
原子性
竞态条件
由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况:竟态条件。
复合操作
“先检查后执行”(例如延迟初始化)和“读取-修改-写入”(例如递增运算)等操作统称为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。
假如有两个操作A和B,如果从执行A的线程来看,当一个个线程执行B时,要么将B全部执行,要么完全不执行B,那么A和B对彼此来说就是原子的。原子操作指:对于访问同一状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作
在实际情况中,应尽可能使用现有的线程安全对象啦管理类的状态。
加锁机制
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
内置锁
Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronzied Block)。同步代码块包括两个部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,无论是正常控制路径退出还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
**Java的内置锁相当于一种互斥体或互斥锁。**当线程A尝试获取一个由线程B持有的锁时,A必须等待或者阻塞,直到B释放这个锁。
**由内置锁保护的同步代码块会以原子方式执行。**任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行由同一个锁保护的同一个同步代码块。
重入
某个线程试图获取一个由它自己持有的锁,这个请求会成功,叫做重入。
重入意味获取锁的操作的粒度是”线程“而不是”调用“。
实现方法:
- 为每个锁关联一个获取计数值和一个所有者的线程。
- 当计数值为0时,这个锁就被认为是没有被任何线程持有。
- 当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。
- 当同一个线程再次获取这个锁,计数值将递增,当线程退出同步代码块时,计数值会递减, 当计数值为0时,这个锁将被释放。
案例:子类对父类方法进行递归调用。调用super.doSomething时无法获取父类的锁,因为已经被持有,从而线程永远停顿下去,等不到锁。重入避免这样错误。
用锁来保护状态
对象的串行访问(Serializing Access)意味着多个线程依次以独占的方式访问对象,而不是并发地访问。
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
每个共享的和可变的变量都应该只由一个锁来保护,从而使得维护人员知道是哪一个锁。
一种常见的加锁约定:将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问。对象状态中国所有变量都有对象的内置锁保护起来。
在不变性条件中的每个变量都必须由同一个锁来保护。在SynchronizedFactorizer类中:缓存的树枝和因数分解结果都由Servlet对象的内置锁来保护。
对于每个包含多个变量的不变形条件,其中涉及的所有变量都需要由同一个锁来保护。
活跃性与性能
通常,在简单性和性能之间存在这相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)。
当执行时间较长的计算或者可能无法快速完成的操作时(例如,网路I/O或控制台I/O),一定不要持有锁。