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. 线程1读取到了inc=10
  2. 线程1被yield(),从执行状态进入就绪状态,换成线程2开始执行。
  3. 线程2读取变量的值,由于线程1还没有修改inc值,所以不会导致线程2的工作内存中缓存变量inc无效,因此线程2读到的inc是10。然后加1。然后根据volatile,立即写入主存。
  4. 轮到线程1执行,由于线程1已经读过了inc的值,且此时inc在线程1的工作内存内为10,线程1没有再读inc,而是直接执行+1操作,得到结果11,写入主存。
  5. 最终:两个线程都执行了一次自增操作,但是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