线程安全需要同时满足三个条件:
- 原子性
某个操作是不可中断的,且要么全部做完要么没有执行。 - 可见性
通过volatile关键字修饰变量实现。读取volatile变量时,先失效本地缓存再读取主存中的最新值;更新volatile变量会立即将最新值刷回主存。 - 有序性
JMM的happens-before原则。
volatile能保证其修饰的变量的线程可见性但无法保证操作原子性,只能用于"多个变量之间或者某个变量的当前值与修改后值之间没有约束"的场景。在实现计数器(++count)和由多个变量组成的不变表达式方面,volatile无法胜任。
volatile的底层实现机制是什么?被volatile修饰的变量在进行写操作时,处理器会插入一条lock前缀的汇编代码,做了层"内存屏障",其作用为:
- 重排序时不能把后面的指令重排序到内存屏障之前的位置
- 将当前处理器缓存行的数据会写回到系统内存。
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
通过处理器之间的缓存一致性协议,当(处理器本)地缓存过期后会失效本地缓存,当更新该缓存时处理器重新从主存load数据到本地缓存并执行计算逻辑。
什么场景下别用volatile?
非线程安全的计数器类
自增操作并不是原子的,比如"++count"操作就是三个原子操作的集合:
- 读取count旧值
- 线程上下文汇总设置count新值: count+1
- 将count新值刷回主存并失效其他线程上下文的count值
假设thread1和thread2均执行"++count"计数操作,thread1和thread2均执行完2但未执行3,此时thread1和thread2先后将count新值刷回主存,这就产生了线程不安全的现象。
只有在状态真正独立于程序内其他内容时才能使用volatile。
// 引用
volatile操作不会像锁一样造成阻塞,因此,在能够安全使用volatile的情况下,volatile 可以提供一些优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。
非线程安全的数值范围类
// 代码来源于Brian Goetz的《正确使用 Volatile 变量》一文,本文稍作修改
@NotThreadSafe
public class NumberRange {
private volatile int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
假设NumberRange初始化后lower/upper分别为0和4,即数值范围为[0,4]。此时thread1和thread2分别执行setLower(3)和setUpper(2),最终lower/upper分别被thread1和thread2设置为3和2,数值范围被更新为[3,2],某种意义上看是一个无效的数值范围。
什么场景下可以使用volatile?
1、对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
2、该变量没有包含在具有其他变量的不变式中。
Case1: 状态标志
这种类型的状态标记的一个公共特性是:通常只有一种状态转换。
// 代码来源于Brian Goetz的《正确使用 Volatile 变量》一文,本文稍作修改
volatile boolean shutdownRequested;
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
Case2: 一次性安全发布 - One-time Safe Publication
// 代码来源于Brian Goetz的《正确使用 Volatile 变量》一文,本文稍作修改
public class Floor {
public Floor() {
// initialization...
}
}
public class Loader {
public volatile Floor floor;
public void init() {
// this is the only write to floor
floor = new Floor();
// initialize floor object here ...
}
public Floor getFloor() {
return this.floor;
}
}
public class SomeOtherClass {
private Loader loader;
public void doWork() {
while (true) {
// use "floor" only if it is ready
if (loader != null) // 步骤1
doSomething(loader.getFloor()); // 步骤2
}
}
}
这是一个典型的读写冲突例子,引文中提到"如果Floor引用不是 volatile 类型,doWork() 中的代码在解除对Floor的引用时,将会得到一个不完全构造的Floor"。我对这句理解的是:thread2执行完步骤2后释放对Floor的引用时,thread1可能正在调用init方法初始化floor,此时thread2拿到的是还未被thread1完全初始化的Floor对象。如果doWork内部主动解除对floor对象的引用,则可能拿到未初始化完全的floor对象的引用。
即使floor对象完成初始化,对floor成员域的修改仍然是线程不可见的。
volatile引用可以保证任意线程都可以看到这个对象引用的最新值,但不保证能看到被引用对象的成员域的最新值。
因为volatile修饰的是floor对象的引用,如果thread1执行到步骤1时,thread3修改了floor成员域,其修改对thread1并不可见。思考:如果floor成员域均被volatile锁修饰,其成员域的修改是否对thread3可见?
Case3: 多个独立观察结果的发布
// 代码来源于Brian Goetz的《正确使用 Volatile 变量》一文,本文稍作修改
public class UserManager {
public volatile String lastUser;
public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
}
return valid;
}
}
这个模式要求被发布的值(lastUser)是有效不可变的 —— 即值的状态在发布后不会更改。与Case1中更新floor对象成员域不同,对String类的操作是在新的String实例上进行的,String对象本身的状态并未改变。String类的这种实现方式天然地提供了线程隔离性。
volatile并非用来解决高并发场景下数据竞争冲突的方案,它只是实现线程可见性的一种方式!如果多个线程同时更新volatile变量,需要采用同步机制解决数据竞争,如CAS或者锁等。
Case4: "volatile bean" 模式
该模式中,Java Bean成员变量均被volatile修饰,且引用类型的成员变量也必须是有效不可变(Collection的子类如List, Set, Queue等有数组值的成员变量,volatile修饰的是数组引用并非数组元素!)。