线程安全性
要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问管理。
竞态条件
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
最常见的竞态条件就是“先检查后执行”操作,另一种竞态条件是“读取-修改-写入”。
数据竞争与竞态条件勿混淆
数据竞争是指,如果在访问共享的非final类型的域时没有采用同步来进行协同,那么就会出现数据竞争。
例如:当一个线程写入一个变量而另一个线程接下来读取这个变量,或者读取一个之前由另一个线程写入的变量时,并且在这两个线程之前没有使用同步,那么就可能出现数据竞争。
并非所有的竞态条件都是数据竞争,同样并非所有的数据竞争都是竞态条件。
复合操作
复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。
例如:“先检查后执行”操作以及“读取-修改-写入”操作。
可以使用同步或者加锁的方式保证复合操作的原子性。
加锁机制
以下为两种加锁方法:
public synchronized void method() {
}
synchronized (锁对象) {
}
内置锁
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或者监视器锁。
Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。
重入
重入的含义?
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。
用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁之后,只能阻止其他线程获得同一个锁。
每个共享的和可变的变量都应该只有一个锁保护。
一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问。
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。
活跃性与性能
通常,在简单性与性能之间存在着互相制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)。
当执行时间较长的计算或者可能无法快速完成的操作时一定不要持有锁。