volatile详解

volatile的3个特性:

  • 保证了各个线程之间的可见性
  • 不能保证原子性
  • 防止重排序

可见性:

首先,每一个线程都有本身的工做内存,除此以外还有一个cpu的主存,工做内存是主存的副本。线程工做的时候,不能直接操做主内存中的值,而是要将主存的值拷贝到本身的工做内存中;在修改变量是,会先在工做内存中修改,随后刷新到主存中。java

注意: 何时线程须要将主存中的值拷贝到工做内存编程

  • 线程中释放锁的时
  • 线程切换时
  • CPU有空闲时间时(好比线程休眠时)

假设有一个共享变量flag为false,线程a修改成true后,本身的工做内存修改了,也刷新到了主存。这时候线程b对flag进行对应操做时,是不知道a修改了的,也称a对b不可见。因此咱们须要一种机制,在主存的值修改后,及时地通知全部线程,保证它们均可以看到这个变化。ide

public class ReadWriteDemo {
    
    //对于flag并无加volatile
    public boolean flag = false;
    public void change() {
        flag = true;
        System.out.println("flag has changed:" + flag);
    }

    public static void main(String[] args) {

        ReadWriteDemo readWriteDemo = new ReadWriteDemo();
        //建立一个线程,用来修改flag,如上面描述的a线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    readWriteDemo.change();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        //主线程,如上面描述的b线程
        while(!readWriteDemo.flag) {
        }
        System.out.println("flag:" + readWriteDemo.flag);
    }

}

按照分析,没有加volatile的话,主线程(b线程)是看不到子线程(a线程)修改了flag的值。也就是说,在主线程看来,在没有特殊状况下,flag 永远为false, while(!readWriteDemo.flag) {}的判断条件为true,系统不会执行到System.out.println("flag:" + readWriteDemo.flag);优化

为了不偶然性,我让程序跑了6分钟。能够看到,子线程确实修改了flag的值,主线程也和咱们预期同样,看不到flag的变化,一直在死循环。若是给flag变量加一个volatile呢,预期结果是,子线程修改变量对主线程来讲是可见的,主线程会退出循环。spa

能够看到,都不到一分钟,在子线程修改flag的值后,主线程随即就退出循环,说明马上感知到了flag变量的变化。线程

有趣的是什么呢:若是ab两个线程间隔时间不长,当b线程也延迟10s读(不是上面的马上读),你会发现两个线程之间的修改也是可见的,为何呢,stakc overflow上有解答,执行该线程的cpu有空闲时,会去主存读取如下共享变量来更新工做内存中的值。更有趣的是,在写这篇文章的时候,cpu及内存是这样的,反而能正常执行,可是能出现问题就能说明volatile的做用。code

image-20210409163242085

如何保证可见性:

首先要先讲一下java内存模型,java的的内存模型规定了工做内存与主存之间交互的协议,定义了8中原子操做:排序

  1. lock:将主内存的变量锁定,为一个线程所独占。
  2. unlock:将lock加的锁定解除,此时其余线程能够有机会访问此变量。
  3. read:将主内存中的变量值读到工做线程中。
  4. load:将read读取到的值保存到工做内存中的变量副本中。
  5. use:将值传递给线程的代码执行引擎。
  6. assign:将执行引擎处理返回的值从新赋值给变量副本。
  7. store:将变量副本的值存储到主内存中。
  8. write:将store存储的值写入到主内存的共享变量中。

我上网查了下资料,也看了不一样的博客,有讲到volatile其实在底层就是加了一个lock的前缀指令。lock前缀的指令要干什么上面也有写。若是对带有volatile的变量进行写操做会怎么呢。JVM会像处理器发送一条lock前缀的指令,a线程就锁定主存内的变量,修改后再刷新到主存。b线程一样会锁定主存内的变量,可是会发现主存内的变量和工做内存的值不同,就会从主存中读取最新的值。从而保证了每一个线程都能对变量的改变可见。内存

原子性:

在编程世界里面,原子性是指不能分割的操做,一个操做要么所有执行,要么所有不执行,是执行的最小单元。rem

public class TestAutomic {
    volatile int num = 0;
    void add() {
        num++;
    }

    public static void main(String[] args) throws InterruptedException {
        TestAutomic testAutomic = new TestAutomic();
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                        testAutomic.add();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        //等待12秒,让子线程所有执行完
        Thread.sleep(12000);
        System.out.println(testAutomic.num);
    }

}

预期现象:都说不能保证原子性了,因此,应该结果是不等于1000

不一样电脑执行的结果不同,个人是886,可能大家的不是,可是都说明了volatile都没法保证操做的原子性。

为何不能保证原子性:

这要从num++操做开始讲起,num++操做能够分为三步:

  • 读取i的值,装载进工做内存
  • 对i加1操做
  • 将i的值写回工做内存,刷新到主存中

咱们知道线程的执行具备随机性,假设a线程和b线程中的工做内存中都是num=0,a线程先抢了cpu的执行权,在工做内存进行了加1操做,还没刷新到主存中;b线程这时候拿到了cpu的执行权,也加1;接着a线程刷新到主存num=1,而b线程刷新到主存,一样是num=1,可是两次操做后num应该等于2。

解决方案:

  • 使用synchronized关键字
  • 使用原子类

重排序:

对于咱们写的程序,cpu会根据如何让程序更高效来对指令经行重排序,什么意思呢

a = 2;
b = new B();
c = 3;
d = new D();

通过优化后,可能真实的指令顺序是:

a = 2;
c = 3;
b = new B();
d = new D();

并非全部的指令都会重排序,重排序与否全是看能不能使得指令更高效,还有下面一种状况。

a = 2;
b = a;

这两行代码不管什么状况下都不会重排序,由于第二条指令是依赖第一条指令的,重排序是创建在排序后最终结果仍然保持不变的基础上。下面将给出volatile防止重排序的例子:

public class TestReorder {
    private static int a = 0, b = 0, x = 0, y = 0;

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            a = 0; b = 0; x = 0; y = 0;
            //a线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                        a = 1;
                        x = b;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }).start();

            //b线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                        b = 1;
                        y = a;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }).start();

            //主线程睡100ms,以保证子线程所有执行完
            Thread.sleep(100);
            System.out.println("a=" + a + ";b=" + b + ";x=" + x + ";y=" + y);

        }
    }

}

还记得上面说过两个线程若是沉睡时间差很少,它们之间是可见

预期结果:

  • 若是先执行a线程(a = 1, x = b = 0),再执行b线程(b = 1, y = a = 1),最终结果a = 1; b = 1; x = 0; y = 1
  • 若是先执行b线程(b = 1, y = a = 0),再执行a线程(a = 1, x = b = 1),最终结果a = 1; b = 1; x = 1; y = 0
  • 若是执行a线程过程(a = 1),接着执行了b线程(b = 1,y = a = 1)【为何y = a必定等于1,由于它们两个之间的改变是可见的】,最后执行了a线程(x = b = 1),最终结果a = 1;b = 1; x = 1; y = 1

能够发现除了上面预期的三种状况,还出现了一种a = 1; b = 1; x = 0; y = 0的状况,相信你们也知道了,这种状况就是由于重排序形成的。要么是a线程重排序先执行x = b;再执行a = 1;,要么是b线程重排序先执行了y = a;再执行了b = 1;;要么是两个线程都重排序了。

若是private volatile static int a = 0, b = 0, x = 0, y = 0;加了volatile关键字会怎么样呢?

为了保证正确性,又持续跑了5分钟,能够发现,确实不会再出现x=0;y=0的状况。

如何防止重排序

先来说讲4个内存屏障的做用

内存屏障 做用
StoreStore屏障 禁止上面的普通写和下面的的volatile写重排序
StoreLoad屏障 禁止上面的volatile写和下面volatile读/写重排序
LoadLoad屏障 禁止下面的普通读和上面的volatile读重排序
LoadStore屏障 禁止下面的普通写和上面的volatile读重排序

可能看做用比较抽象,直接举例子叭

  • 对于S1; StoreStore; S2,在S2及后续写入操做以前,保证S1的写入操做对其它线程可见。
  • 对于S; StoreLoad; L,在L及后续读/写操做以前,保证S的写入对其它线程可见。
  • 对于L1; LoadLoad; L2,在L2及后续读操做以前,保证L1读取数据完毕。
  • 对于L; LoadStore; S,在S及后续操做以前,保证L读取数据完毕。

那么volatile是如何保证有序性的呢?

  • 在每一个volatile写操做前插入StoreStore屏障,每一个写操做后面加一个StoreLoad屏障。
  • 在每一个volatile读操做前插入LoadLoad屏障,在读操做后插入LoadStore屏障。

举例,有个对volatile变量的写S,有个对volatile变量的读L,会怎么样呢。

  • 对于写:S1; StoreStore; S ;StoreLoad L这样可以把S(对volatile变量保护在中间)防止重排序。
  • 对于读同样的道理:L1; LoadLoad; L ; LoadStore S,同样把volatile变量保护的好好的。

有关volatile的讲解就到这里了。

相关文章
相关标签/搜索