VOLATILE IN JAVA
VOLATILE 关键字的主要作用
1、保证变量在多个线程间操作的可见性。
2、禁止指令重排序。
前置知识
JAVA 内存模型 JMM(Java Memory Model)
每条线程有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者之间的交互关系如上图。
因此,当两个线程 A、B 均需要操作同一个共享变量 X 时,就会出现:A 线程在自己的工作内存中对 X 的值进行了修改,并立刻同步给了主内存,但是此时并不能保证 B 线程要立刻去主内存中获取新的 X 值,这时线程 B 的工作内存中的 X 值依然是旧值。
使用 volatile 修饰的变量可以保证在一个线程修改了这个变量的值后,这新值对其他线程来说是立即可见的。
public class T01_HelloVolatile {
//对比一下有无volatile的情况下,整个程序运行结果的区别
volatile boolean running = true;
void m() {
System.out.println("m start");
while(running);
System.out.println("m end!");
}
public static void main(String[] args) {
T01_HelloVolatile t = new T01_HelloVolatile();
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
}
}
volatile 实现线程间变量可见性底层原理
volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。
读屏障、写屏障?
- 内存屏障,又称内存栅栏,是一个 CPU 指令。
- 在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。
注意:volatile 保证线程间操作的可见性,但是无法保证操作的原子性。
可见性只能保证每次读取的是最新的值,但是 volatile 没办法保证对变量的操作的原子性。如自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:
1、假如某个时刻变量 inc 的值为 10。
2、线程 A 对变量进行自增操作,线程 A 先读取了变量 inc 的原始值,然后线程 A 被阻塞了。
3、然后线程 B 对变量进行自增操作,线程 B 也去读取变量 inc 的原始值,由于线程 A 只是对变量 inc 进行读取操作,而没有对变量进行修改操作,所以不会导致线程 B 的工作内存中缓存变量 inc 的缓存行失效,所以线程 B 会直接去主存读取 inc 的值,此时 inc 的值时 10,然后进行加 1 操作,并把 11 写入工作内存,最后写入主存。
4、然后线程 A 接着进行加 1 操作,由于已经读取了 inc 的值,注意此时在线程 A 的工作内存中 inc 的值仍然为 10,所以线程 A 对 inc 进行加 1 操作后 inc 的值为 11,然后将 11 写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,inc 只增加了 1。
为什么会有指令重排
什么情况下需要禁止指令重排
如在 双重检查锁的单例模式下需要禁止对象初始化过程中可能发生的指令重排序。
public class LazydoubleCheckSingleton {
//加上volatile关键字,就不允许2 3 有重排序的可能
private volatile static LazydoubleCheckSingleton lazydoubleCheckSingleton;
private LazydoubleCheckSingleton() {
}
public static LazydoubleCheckSingleton getInstance() {
if (lazydoubleCheckSingleton == null) {
synchronized (LazydoubleCheckSingleton.class) {
if (lazydoubleCheckSingleton == null) {
//1.分配内存给这个对象
//2.初始化这个对象
//3.lazydoubleCheckSingleton指向刚分配这个对象的内存地址
lazydoubleCheckSingleton = new LazydoubleCheckSingleton();
}
}
}
return lazydoubleCheckSingleton;
}
}
如果 2、3 两步中发生的指令重排,就会导致有可能某个线程获取到的这个对象是没有初始化完成的状态的。
volatile 禁止指令重排的底层实现
volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的 赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile 又保证了可见性,所以就可以保证线程安全了。