Java多线程之volatile

在学习Volatile之前有必要简单了解一下物理内存模型和Java的内存模型,这样对理解Volatile大有好处。

寄存器

    首先我们要知道的是所有运算操作都是在CPU的寄存器中进行的,而CUP的执行涉及到数据的读取和写入两个步骤。CUP能访问到的所有数据都在计算机的主存当中,但是由于直接读取主存会很慢(相对于CPU缓存来说),所在在CPU和主存之间增加了一层Cache层,就是CPU缓存,CPU缓存一般有2级甚至3级,每一级的大小和效率各不相同,这里我们可以先不去了解,只需要统一当做是CPU缓存即可。
    当CPU的缓存的引入后那么计算会怎样呢?首先把运算所需要的数据从主存中复制一份到CUP Cache,然后CPU直接对Cache中的数据进行运算操作,当运算结束时再把数据刷新回主存当中。大概流程如下图:

Java多线程之volatile_第1张图片

    这在单线程的场景下是没有问题的,但是如果是多个线程同时操作共享变量就会出现并发问题,例如现在有变量i=0 ,有两个线程对变量进行i++运算,线程A把i加载到本地内存(线程内存,类似CUP Cache),对i进行+1操作,而线程B同样的把i加载到本地内存,对i进行+1操作,此时在A的本地内存中i的值为1,在B的本地内存中i的值同样是1,这是A和B线程把本地内存中的i刷新到主内存,所以最终i的值是1,而不是我们期望的2。

java内存模型

Java多线程之volatile_第2张图片

    java的内存模型是一个抽象的概念,跟硬件的内存模型类似,它涵盖了缓存,寄存器,编译器优化以及硬件等。java内存模型跟计算机硬件的结构并不完全一样,例如计算机物理内存没有栈内存和堆内存的划分,这两者都对应到物理的主内存,当然也有可能有一部分是对应到CPU Cache和寄存器中。如下图
Java多线程之volatile_第3张图片

并发的三大特性
1.原子性

原子性是指在一次操作或多次操作中,要么所有操作都执行成功,要么所有操作都不执行。
1) x = 1
线程把x=1写入到工作内存,然后再刷新到主内存。这个赋值为原子性操作
2) x++ 运算
x++操作涉及到3个步骤:
1,首先把x从主内存中加载到工作内存(线程内存)。
2,对x进行加1运算。
3, 把x刷新到主内存。
假设x的初始值为1,在步骤2的时候有可能有其他线程同样的x++操作,最终两个线程把x刷新到主内存后x的值为2,而不是预期期望的3。所以x++不是原子性的。
3) x = x + 1 运算
同x++一样,非原子性。
4) y = x
该赋值操作包含两个步骤
1,把x从主内存加载到工作内存。
2,把x的值赋给y,再把y刷新到主内存。
虽然两步都是原子性的,但是合起来就不是原子性的了。

从上面可以看出:
1,多个原子性操作在一起就不再是原子性操作。
2,简单的读取赋值操作是原子性,把一个变量赋给另一个变量就不是原子性的。

结论:volatile不具备原子性语义

2.有序性

我们在编写程序的时候,原则上代码逻辑是由上到下执行,但是在Java的内存模型中,允许编译器和处理器对指令进行重排来优化程序的执行,在单线程的情况下,重排不会对执行结果造成影响,但是在多线程下,指令重排就有可能影响到最终的结果了。
如下面代码:

private boolean flag = false;
private Object obj;

public Object getObj(){
    if(!flag){
      //1   
      obj = new Object();
       //2 
       flag = true;
    }
    return obj;
}

原始代码经过指令重排后可能会变成

public Object getObj(){
    if(!flag){
      //1   
       flag = true;
      //2 
      obj = new Object();
    }
    return obj;
}

单线程下返回的obj永远不会为null,但是多线程下,当线程A执行到if(!flag)时,线程B就刚好执行完注解1的代码,而还未执行注解2的代码,这是线程A的条件不满足,直接返回obj,而这时obj还是为null,会有NPE的风险错误。
如果变量采用volatile修饰,如下:

private volatile boolean flag = false;

那么就保证

flag = true;

 obj = new Object();

之后执行。
总体概括volatile的确保有序性逻辑如下:

private volatile int i=0;

方法体{
//a代码片段
//b代码片段
//c代码片段
i = 2;
//d代码片段
//e代码片段
//f代码片段
}

volatile能确保 a,b,c在i=2赋值之前执行,有可能经过指令重排后执行顺序是a,c,b或者b,a,c。无论哪种顺序都保证在i=2赋值语句执行之前完成执行。而d,e,f保证在赋值语句之后执行,同样d,e,f的执行的顺序也可能经过重排。
结论,volatile保证有序性

3.可见性

一般线程对变量进行运算是需要先把变量从主内存加载到工作内存,然后运算完再刷回到主内存中,但是什么时候刷回主内存是不确定的,在刷回主内存之前,其他线程是看不到变量的变化的。
volatile修饰的变量,在工作内存中修改完会立刻刷回到主内存,同时会把其他线程的工作内存中的共享变量致为失效,致使其他线程需要重新从主内存中加载最新的值。保证可见性的还有另外两中方式synchronized和Lock的lock
结论,volatile保证可见性

如果对你有用的,麻烦点收藏,你的支持是我创作的动力,谢谢

参考资料
《Java高并发编程详解》

你可能感兴趣的:(java,多线程,volatile,并发)