volatile关键字修饰的变量可以保证可见性
变量用volatile修饰得时候,当一个线程修改了变量,其他线程可以立即读到修改后得值,适用于一写多读得多线程场景。
多线程对变量有复杂操作禁止使用,容易引起线程间并发的一些问题
当一个线程修改了volatile修饰的变量时,cpu运算后会立即写到共享主存中,其他cpu读取的时候会先将自己独有内存的变量副本置为不可用,直接从共享主存中读取数据,可以理解为对于单一的读写操作是只对共享主存操作的
总结:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入
volatile关键字修饰的变量保持原子性
以上图片两边代码可以理解为等同当只对变量进行set操作的时候可以保证变量的原子性
以下是当两个线程并发+1操作的情况,线程1和线程2同时对volatile修饰的变量+1,会先将变量读取到cpu运行空间计算,运算完写入到主存,由于并发读取到cpu中的变量都为1,计算完都为2,所以结果为2,但是我们期望为3,就会造成业务异常
总结:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
volatile修饰防止指令重排序
对一个volatile域的写,happens-before于任意后续对这个volatile域的读。对于volatile关键字修饰的变量而言,后续的任意对这个变量的读操作必须在这个写操作后边
当程序执行到 volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
例如:双重校验单例模式,对象必须使用volatile关键字修饰
uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}