volatile解决可见性和有序性问题

Java内存模型

Java内存模型就是围绕在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。Java内存模型定义了8种内存访问操作以及其执行时需要满足的诸项规则,以及对volatile的特殊规定,这些足以判定哪些内存操作在并发下是安全的。当然,我们并不需要以上述的方式来思考并发问题,我们只需要用先行发生原则(Java内存模型定义的一个等效判断原则)来确定一个操作在并发环境下是否安全即可。

先行发生原则

先行发生(Happens-before)原则指的是什么?

A Happens-before B就意味着A产生的影响可以被B观察到,也就是说A对B是可见的。

Java内存模型下的Happens-before原则:

  1. 程序的顺序性规则: 在一个线程中,按照程序的控制流顺序,前面的操作Happens-Before于后续的任意操作。

  2. volatile变量规则:对一个volatile变量的写操作,Happens-Before于后续对这个变量的读操作。(后续是指时间上的先后)

  3. 传递性规则:如果A Happens-Before B,且B Happens-Before C,那么 A Happens-Before C。

  4. 管程中锁的规则:一个锁的解锁Happens-Before于后续这个锁的加锁。(后续是指时间上的先后)

  5. 线程start()规则:主线程A启动线程B的start()方法(即在线程A中启动线程B),那么该start()操作Happens-Before于线程B中的任意操作。

  6. 线程join()规则:主线程A等待子线程B完成(主线程A调用子线程B的join()方法),当子线程B完成后,主线程能够看到子线程的操作(指的是对共享变量的操作)。

  7. 线程中断规则:对线程interrupt()方法的调用先行发生与被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测是否发生中断。

  8. 对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定Happens-Before它的finalize()方法。

注意:先行发生(Happens-Before)原则与时间先后顺序没有因果关系,我们衡量并发问题时要以Happens-Before原则为准。

volatile

volatile关键字可以说是最轻量级的同步机制,可以用来解决可见性问题,以及用来保证部分程序的有序性。

volatile解决可见性问题

volatile最原始的意义就是禁用 CPU 缓存,这意味着对于volatile修饰的变量,对于其读写,都必须从内存中读取或写入,而不能使用CPU缓存。

从volatile的含义可以得知使用volatile可以有效的解决可见性问题。在之前的可见性问题的例子中,只需要对stop变量添加volatile关键字即可,代码如下。

VisibilityTest.java

public class VisibilityTest {
    public static void main(String[] args) throws Exception {
        VisibilityThread v = new VisibilityThread();
        v.start();

        //停顿1秒等待新启线程执行
        Thread.sleep(1000);
        System.out.println("即将置stop值为true");
        v.stopIt();
        Thread.sleep(1000);
        System.out.println("finish main");
        System.out.println("main中通过getStop获取的stop值:" + v.getStop());
    }
}


VisibilityThread.java

public class VisibilityThread extends Thread{
    private volatile boolean stop = false;
    @Override
    public void run() {
        int i = 0;
        System.out.println("start loop.");
        while(!getStop()) {
            i++;
        }
        System.out.println("finish loop,i=" + i);
    }

    public void stopIt() {
        stop = true;
    }

    public boolean getStop(){
        return stop;
    }
}

volatile解决有序性问题

另外,volatile还可以在一定程度上解决有序性问题。

我们关注的有序性自然不是时间上的执行先后顺序,而是前序操作对于后续操作的影响,这就与Happens-before原则不谋而合。A先于B,这种有序性的实质就是保证A所产生的影响对B是可见的。

volatile解决有序性问题依赖的就是上述提到的Happens-before规则中的前3条规则,下面通过代码来具体分析volatile是如何利用这三条规则来保证有序性的。

int x = 0;
    volatile boolean v = false;
    public void writer() {
        x = 42;
        v = true;
    }
    public void reader() {
        if (v) {
            // 这里x会是多少呢?
                System.out.println(x);
        }
    }

在上述代码,x会取到什么反常的值(没有volatile的情况下)?(x有可能等于0,这是为什么?)

这自然是因为编译优化带来的指令重排,导致了v = true操作先于x = 42执行(时间上先后),而reader()操作正好在两者之间执行,这样就可能出现x=0的情况。解决这个问题只需要用volatile对v进行修饰即可。使用volatile修饰后,再来分析这段代码。

  1. 首先看Happens-Before规则的第二条(对一个volatile变量的写操作,Happens-Before于后续对这个变量的读操作),这里需要注意的是怎么确定volatile变量操作的时间前后顺序。

    • 通过赋值+值的判断来确定
    • 如上述代码片段中(已加volatile关键字),writer()中对v进行了赋值,reader()中判断v的值,当判断v值确实是true后,则说明v的写操作在时间上先于其读操作发生。这种情况下,v = true Happens-Before if(v)。
  2. 之后结合Happens-Before规则的第一条,可以得出

    • x = 42 Happens-Before v = true
    • v = true Happens-Before if(v)(判断为true时)
    • if(v) Happens-Before System.out.println(x)
  3. 再结合Happens-Before规则的第三条,可以得出

    • x = 42 Happens-Before System.out.println(x)
    • 所以此处x取值只能是42。

总结

  • volatile可以解决可见性问题。
  • volatile可以解决部分有序性问题,解决思路:
    • 确定在不同线程X和Y中具有依赖关系的操作A和B的先后顺序(如A Happens-Before B)
    • 取一个共享的布尔(也可以是其他类型)变量flag并赋初值
    • 线程X中在A操作后改变flag的值
    • 线程Y在B操作前判断flag的值是否为上一步改变的值
    • 这样就可以保证具有依赖关系的A和B操作有序执行

版权声明:本文为zcyt085原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。