volatile作用:
我们看到的最多的解释volatile具备两个特性:volatile保证内存可见性、禁止指令重排!
那我们就来详细了解这两个特性:首先来了解内存可见性!
内存可见性:
可见性:指线程之间的可见性,当多个线程访问同一个变量时,当一个线程修改了该共享变量的值,新值对于其他线程来说是可以立即得知的。
那既然volatile关键字能够保证内存可见性,那我就来个例子看看是否可以保证内存可见性:
example1:当我们的flag变量没有用volatile修饰的时候:
main线程先睡眠(这里是为了让线程A读取到未修改的值),从主内存加载flag的值”0“到工作内存并修改为“1”,更新到主内存中;
线程A从主内存加载flag的值为“0”到自己的工作内存中(因为main线程睡眠,所以这里读取的值是main线程还未修改值,这里就是Thread.sleep(1000)的作用),所以我们的线程A工作内存的flag一直为“0”,我们的while循环是一直满足条件的,所以控制台肯定是不会停止的会一直在循环!
public class TestVolatile3 {
//volatile
private static int flag = 0;
private static int testNum = 0;
private static int testNum2 = 0;
public static void main(String[] args) throws Exception {
new Thread(()->{
while (flag == 0){
}
},"线程A").start(); //
Thread.sleep(1000);
// testNum =1;
/**
* main线程修改了flag的值,并更新到主内存中,那么线程A能够读取到main线程修改后的值吗?
* 肯定读取不到的,所以这里后台会一直在循环!后台不会停止
*/
flag = 1;
// testNum2 = 1;
}
}
example1.1:当我们把flag变量用volatile关键字修饰,我们看结果:
对上面的结果可以证实 volatile确实具备了 可见性的特性,那我们就来看看volatile底层到底是怎么做的,怎么实现可见性的!
刨根底层实现
我们用javap 查看的时候会发现加不加volatile关键字 生成的字节码都是一样的,所以我们要查看生成的汇编语言到底是怎么样的
需要引入hsdis-amd64.dll -XX:+PrintAssembly 参数输出反汇编
在我们的jvm参数里面添加:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:-Inline
我们运行后会发现加了volatile的汇编会多出” lock addl $0x0,(%rsp) “ 操作(这里是写的操作,该操作的作用相当于一个内存屏障Memory Barrier或Memory Fence:指重排序后不能把后面的指令重排序到内存屏障之前的位置)我们下面来看看这个操作的含义:
lock addl $0x0,(%rsp) 指令,这个指令中的“addl $0x0,(%rsp)”(把rsp的寄存器的值加0)这个是一个空操作,主要是前面的lock前缀,它的作用是将本处理器的缓存写入内存,该写入动作也会引起别的处理器或者别的内核无效化其缓存。所以通过这样一个空操作,可让前面volatile变量的修改对其他处理器立即可见。所以就实现了所谓的 " 内存可见性 “!!!!!!!
所以我们可以得出这个指令(内存屏障):这个保证了两个事情,第一,不会重排序。第二,所有的变量值都会回写到主内存中,从而在这个指令之后,变量值对其他线程可见。
下班了