volatile关键字 —— 可见性及禁止指令重排序

提起volatile关键字,我们肯定就想到这是我们Java中,最轻量级的同步机制,但是它真的能保证我们的线程安全么?

显然不可以,我们在多线程编程中,最注重的问题就是:原子性、可见性、有序性。

原子性

我们volatile关键字其实是无法保证我们的原子性的,如下例

public class App {
    public static void main( String[]  args ) throws ExecutionException, InterruptedException {
        TestVolatile testVolatile = new TestVolatile();
        Thread t1 = new Thread(testVolatile);
        Thread t2 = new Thread(testVolatile);
        Thread t3 = new Thread(testVolatile);
        Thread t4 = new Thread(testVolatile);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

class TestVolatile implements Runnable{
    volatile int i = 0;
    @Override
    public void run() {
        i++;
        System.out.println(Thread.currentThread().getName()+"..."+i);
    }
}

volatile关键字 —— 可见性及禁止指令重排序_第1张图片
从结果中看我们就可以发现 volatile 并没有保证我们的原子性,因为我们 i++ 并不是一个原子操作,在我们执行 i++ 实际上分为了三步—— 1、获取 i 的值,2、i 值加1,3、将值赋给 i 。

而我们的synchronized 关键字就可以保证其原子,如下
volatile关键字 —— 可见性及禁止指令重排序_第2张图片
volatile关键字 —— 可见性及禁止指令重排序_第3张图片



可见性

那我们 volatile 关键字有什么作用呢?其实我们的 volatile 关键字最主要作用,就是其可见性,说到其可见性,我们需要简单了解一下我们CPU的内存模型,我们知道在计算机执行程序的时候,指令都是在CPU中执行的,但是我们的数据是存储在计算机的主内存中(物理内存)的。这就是导致一个不可避免的问题,就是内存的读取和写入的速度与CPU的执行指令速度相比差距是很大的,这样就会造成了与内存交互时程序执行效率大大降低,因此在CPU中就设计了高速缓存。
volatile关键字 —— 可见性及禁止指令重排序_第4张图片

高速缓存什么意思呢?就是我们CPU在执行的时候,会把主内存中的数据复制一份到我们的CPU高速缓存中去,如果我们有多个线程去执行一个程序,那么我们每个线程都复制了一份到高速缓存中,如我们现在有二个线程去执行一个程序,目的是每个线程把 num 的值加 1 ,num 的默认值是 0,结果应该是 num=2,但是可能有这种情况,我们的线程1 把 num=0 存入高速缓存中,进行加 1,现在线程1高速缓存中的 num=1,但是线程还没写入主内存中,主内存中还是num=0,线程2又把 num=0 存入高速缓存中,进行加1,现在线程2高速缓存中的也 num=1,然后线程1高速缓存写入主内存,主存中 num=1,然后线程2高速缓存写入主内存,主内存还是 num=1。



那我们 volatile 是如何解决这个问题的呢?我们的 volatile 关键字主要是利用了CPU的缓存一致协议,就是我们主存中 num=0,然后我们线程1、线程2都将其存入其高速缓存中。

假设我们线程1运行将其加 1,num=1了,这时因为加了volatile关键字,我们线程1的高速缓存会立即写入主内存中(注意: 我们该线程不仅会将volatile关键字修饰的变量立即写入主内存,还会把当前线程(方法)中所有可见的变量立即写入主内存中),并且通过我们其他的线程 num 的值被我改啦,你们之前拿到的值都不可能用啦,去主内存中重新取下值吧。

然后到我们线程2运行,因为收到了通知,其高速缓存的 num=0 已失效,不能用啦,就又去主存中又去了一次,num=1,然后对其加1,num=2,并将其写入主存中,也会通知其他线程 num 的值又被改啦。



由以上的例子,我们可以总结出我们 volatile 关键字最适合使用的创建就是,只有一个线程写,多个线程读的场景,因为它只能确保可见性。




禁止指令重排序

我们 volatile关键字还有一个作用,就是禁止指令重排序,也就是我们上面说的保证其有序性,有序性:即程序执行的顺序按照代码的先后顺序执行。


那什么叫指令重排序呢?

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,在单线程下有着一个 as-if-serial 的概念 。

as-if-serial 的概念就是: 不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守 as-if-serial 语义。为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。


在什么情况下我们会进行重排序呢?

int a = 3, b = 5;
a = 8;
b = 1;

我们可以看见 a=5 是在 b=7 的前面,但是在真正的执行顺序并一定是 a=5 先执行,也可能是 b=7先执行。因为 a 和 b 之间不存在数据依赖关系。a=8 和 b=1 谁先执行对我们的结果没有任何的影响,所以会被重排序。


然后我们再来看一下存在数据依赖关系的例子,如下

int a = 3, b = 5;
a = b + 8;
b = 1;

但是上述情况就不会被重排序,因为我们 a 会依赖 b 的值,重排序会影响我们的代码的执行结果。




除了上述的数据依赖,还有就是我们存在控制依赖的情况下,也不是不应该进行重排序,简单的就是我们所说的 if 条件,如下:就是我们不会将 a = 10 先进行,然后再判断条件,这是显然的。


但是我们从执行顺序中也不是一定的,还是有可能先执行 a = b + 8 的,当然它不是直接运算赋值,而是会将这个运算出来的结果先存入我们的重排序缓存器之中,然后接下来发现如果 if (flag) 成立的话,就直接把结果拿出来直接用,否则就不用啦。

if (flag){
	a = b + 8;
}



刚刚以上都是说的在单线程下的重排序的例子,都不存在其安全问题,那么在多线程下呢?会不会不一样,存在安全问题呢?


如下首先我们先定义两个变量,然后我们在不同的线程中对其进行操作

int a = 3, b = 5;

首先让线程1来运行下列逻辑,判断其变量a和b的大小,当ab时,才会跳出死循环,然后返回b的值。

//线程1
while (a < b){
}
return b;

这时线程1肯定一直不会结束,这里我们在创建一个线程2来改变变量a、b的值,在运行下列线程2的代码时,这时根据上述的分析,这里代码是有可能会进行指令重排序的。因为这里既不存在数据依赖关系,也不存在控制依赖关系。

//线程2
a = 8;
b = 1;

在线程2正常不进行指令重排序的情况下,线程1跳出while循环时的值,可能是 a=8 , b=5 ,那么返回值可能是b=5

但是如果线程2发生了指令重排序的情况,这里线程1跳出while循环时的值,可能是 a=3 , b=1,那么返回值可能是b=1


注意: 上述代码只能简单的帮助我们理解多线程下,如果发生指令重排序的不同结果,可能导致的不同情况,但是这里的代码运行结果并一定表示发生了指令重排序,什么意思呢?

就是当返回值是b=1,只能表示有可能发生了指令重排序,因为在没有发生指令重排序时,跳出while循环时a=8,b=5,但是这里不要忘记了线程1还在继续运行,这里线程1继续向下就会将b的值进行更新,即b=1

当返回值是b=5时,同样也不能保证一定未发生指令重排序,原因同上。



之所以用上述例子来介绍,只是我认为这个代码比较简单,容易理解,如果想要用代码来演示指令重排序的情况,可以尝试多运行几次下列代码,只要控制台打印出来了相关语句,那么就说明一定发生了指令重排序

public class SimpleHappenBefore {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 500000; i++) {
            SimpleHappenBefore.State state = new SimpleHappenBefore.State();
            ThreadA threadA = new ThreadA(state);
            ThreadB threadB = new ThreadB(state);
            threadA.start();
            threadB.start();

            threadA.join();
            threadB.join();
        }
    }

    static class ThreadA extends Thread {

        private final SimpleHappenBefore.State state;

        ThreadA(SimpleHappenBefore.State state) {
            this.state = state;
        }

        public void run() {
            state.a = 1;
            state.b = 1;
            state.c = 1;
            state.d = 1;
        }
    }

    static class ThreadB extends Thread {

        private final SimpleHappenBefore.State state;

        ThreadB(SimpleHappenBefore.State state) {
            this.state = state;
        }

        public void run() {
            if (state.b == 1 && state.a == 0) {
                System.out.println("b=1");
            }

            if (state.c == 1 && (state.b == 0 || state.a == 0)) {
                System.out.println("c=1");
            }

            if (state.d == 1 && (state.a == 0 || state.b == 0 || state.c == 0)) {
                System.out.println("d=1");
            }
        }
    }

    static class State {
        int a = 0;
        int b = 0;
        int c = 0;
        int d = 0;
    }
}

你可能感兴趣的:(并发编程)