JMM内存模型
在了解Volatile之前,需要先了解JMM内存模型,在前面的文章中已经做了较为详细的描述。
Volatile关键字的语义
保证可见性
被Volatile修饰过的变量被一个线程修改后,新值对其他线程而言立即可见。
例如下列代码:
//线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true;
代码的本意是用线程2来完成线程1的中断。但是并非一定能成功。每个线程在运行过程中有自己的工作内存,线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。当线程2更改了stop变量的值之后,如果但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
如果用volatile修饰stop变量就能保证中断的成功发生了。线程2修改stop变量后会立即写入主存,导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效),所以线程1再次读取变量stop的值时会去主存读取,得到最新的正确的值。
禁止重排序
对被volatile修饰过的变量进行读、写操作时,禁止JVM进行重排序,具体如何禁止如下:
- 第一个操作为volatile变量的读时,第二个任何操作不可重排序到第一个操作前面。
- 第二个操作为volatile变量的作时,第一个任何操作不可重排序到第二个操作后面。
- 第一个操作为volatile变量的写时,第二个的volatile变量的读写操作不可重排序到第一个操作前面。
volatile与并发编程的三个性质
可见性
volatile对可见性的保证是显然的。一个线程更新变量变量后会立即对其他所有线程可见正是volatile的最重要的作用。
原子性
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); //创建10条线程,每条线程并行地执行1000遍自增操作 for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } // 不断尝试将线程从执行状态拉回就绪状态 while(Thread.activeCount()>1) Thread.yield(); System.out.println(test.inc); } }
上述代码运行结果会得到小于10000的数。没错,如果线程将inc值更新,会让其他所有线程读到最新的inc值。但是问题出在,自增的操作包含:读取变量的原始值、进行加1操作、写入工作内存。假设:
- 线程1读取到了inc=10
- 线程1被yield(),从执行状态进入就绪状态,换成线程2开始执行。
- 线程2读取变量的值,由于线程1还没有修改inc值,所以不会导致线程2的工作内存中缓存变量inc无效,因此线程2读到的inc是10。然后加1。然后根据volatile,立即写入主存。
- 轮到线程1执行,由于线程1已经读过了inc的值,且此时inc在线程1的工作内存内为10,线程1没有再读inc,而是直接执行+1操作,得到结果11,写入主存。
- 最终:两个线程都执行了一次自增操作,但是inc只加了1。
线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。因此根源在于:自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
有序性
因为volatile具有禁止重排序的语义,具体规则也在上文提及,所以volatile是可以保证一定程度上的有序性的。下面通过两个例子说明volatile对有序性的保证。
1.
//x、y为非volatile变量 //flag为volatile变量 x = 2; //语句1 y = 0; //语句2 flag = true; //语句3 x = 4; //语句4 y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
2.
//线程1: context = loadContext(); //语句1 inited = true; //语句2 //线程2: while(!inited ){ sleep(); } doSomethingwithconfig(context);
上例可能出现的问题是,有可能语句2会在语句1之前执行,那么就可能导致context还没被初始化,线程2中就使用未初始化的context去进行操作,导致出错。这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。
结论
基于volatile的两大语义,可以保证可见性与一定程度的有序性,但不能保证对volatile变量操作的原子性。
参考
https://www.cnblogs.com/dolphin0520/p/3920373.html
https://baijiahao.baidu.com/s?id=1663045221235771554&wfr=spider&for=pc