volatile关键字详解

前言

我们知道,操作系统主要由CPU、I/O设备,主存等组成。但是由于CPU的处理速度与内存的读取速度相差好几个量级,而且CPU是非常宝贵的资源。因此在内存与CPU之间引入了高速缓存,内存将待处理的数据放入高速缓存,cpu从高速缓存读取数据,借以充分利用CPU的资源。

CPU的高速缓存分为L1 cache/L2 cache/L3 cache。它们的存储大小L1L2>L3。

volatile关键字详解_第1张图片

正是由于引入了高速缓存,在多线程环境下,由于是每一个cpu核心有一个自己的高速缓存区,可能会出现从内存中读到的数据不是最新数据的情况

举例:

多个线程 对Integer i=0 进行+1操作,如果这里我们有三个cpu核心,那么理想情况下 返回的数据是3

但是多线程 是会存在并发的,假如说cpu0后的变量 未被及时刷新到内存中,cpu1或cpu2直接把初始的变量加载进来了,从而导致最终计算的结果不准确。

我们把这类问题称之为缓存一致性问题,解决缓存已知悉的方案之一就是使用volatile

volatile关键字介绍

volatile关键字是Java提供的一种轻量级同步机制。它能够保证可见性有序性,但是不能保证原子性

可见性

即某个线程对共享变量的修改会被立即刷新到主内存中,并让其他线程强制加载最新的主内存变量数据。

举例说明

//内存不可见性举例
public class VolatileDemo {

    private Integer num = 0;

    public static void main(String[] args) {

        VolatileDemo demo = new VolatileDemo();
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t come in");
                try {
                    Thread.sleep(1 * 1000);
                    demo.setNum();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t update number value: " + demo.num);
            }, "MyThread" + i).start();
        }
			//主线程在此判断,如果拿到了修改后的值 则循环结束
        while (demo.num == 0) {
        }

        System.out.println("num=" + demo.num);

    }

    public void setNum() {
        this.num = 10;
    }

}

输出结果如下:

MyThread0 come in
MyThread1 come in
MyThread2 come in
MyThread1 update number value: 10
MyThread2 update number value: 10
MyThread0 update number value: 10

会发现程序被阻塞在while循环处。

起初main线程与三个子线程从内存中拿到的num值都是0,而后来三个子线程分别把num值都进行了修改,但是似乎main线程中拿到的num数据还未被更改(程序阻塞在while循环处就是证明)。

这也就证明了该案例不具备内存可见性

我们为num变量添加volatile修饰之后,再来看下

public class VolatileDemo {

    private volatile Integer num = 0;

    public static void main(String[] args) {

        VolatileDemo demo = new VolatileDemo();
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {

                try {
                    System.out.println(Thread.currentThread().getName() + "\t come in");
                    Thread.sleep(1 * 1000);
                    demo.setNum();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t update number value: " + demo.num);
            }, "MyThread" + i).start();
        }

        while (demo.num == 0) {
        }

        System.out.println("num=" + demo.num);

    }

    public void setNum() {
        this.num = 10;
    }

}

结果:

MyThread0 come in
MyThread1 come in
MyThread2 come in
MyThread1 update number value: 10
MyThread0 update number value: 10
MyThread2 update number value: 10
num=10

可见,此时某个线程对num的修改会立刻反映到主内存上

原子性

volatile并不能保证操作的原子性

举例说明:1000个线程对num进行加1操作

public class VolatileDemo {

    private volatile Integer num = 0;

    public static void main(String[] args) {

        VolatileDemo demo = new VolatileDemo();
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                demo.add();
            }, "MyThread" + i).start();
        }

        System.out.println("num=" + demo.num);

    }

    public void add() {
        this.num = this.num + 1;
    }

}

按照预期的话 结果应该是1000,但是实际执行的结果总是<=1000

为什么实际结果会<=1000呢?

首先num=num+1并不是一个原子操作,而是三个操作。

当一个线程A在自己工作内存执行完+1操作后(假如结果是2),要写入主内存时,可能线程B抢占了cpu,然后线程B 执行完+1操作(比如结果是3),写回了主内存,而线程A此时得到了CPU的执行权,它接着从之前未执行完的地方执行(把2写入主内存)。这个时候它就出现了写覆盖,将2覆盖了现在主内存的3。然后其他线程从主内存基于这个数字再运算,因此得到的结果可能比实际小。

解决方案:对add方法加锁或者使用原子类(后面说)

public class VolatileDemo {

    private volatile Integer num = 0;

    public static void main(String[] args) {

        VolatileDemo demo = new VolatileDemo();
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                demo.add();
            }, "MyThread" + i).start();
        }

        System.out.println("num=" + demo.num);

    }

    public synchronized void add() {
        this.num = this.num + 1;
    }

}

这样就能保证计算结果的正确性了。

有序性

volatile可以保证有序性,也就是防止指令重排序。所谓指令重排序,就是出于优化考虑,CPU执行指令的顺序会与我们程序的顺序不一致,规则是CPU将前后没有依赖关系的指令进行重排,以期达到优化的目的。

举例:

int x = 11; //语句1
int y = 12; //语句2
x = x + 5;  //语句3
y = x * x;  //语句4

实际的CPU执行顺序会有多种,如1234、2134等。但不会将1与3的指令执行顺序颠倒,因为语句3是依赖语句1的。

voliatile使用举例

DCL 双重检查锁 即用到了volatile

public class DCL {

    private volatile static DCL dcl = null;

    private DCL() {
    }

    //Double Check Lock 双重检查锁机制:在加锁前后都进行判断
    public static DCL getInstance() {
        if (Objects.isNull(dcl)) {
            synchronized (dcl) {
                if (Objects.isNull(dcl)) {
                    dcl= new DCL();
                }
            }
        }
        return dcl;
    }
}

之所以会用到volatile关键字,主要是因为dcl=new DCL() 不是原子性的,是多个指令的集合。

memory = allocate();     //1.分配内存
instance(memory);	 //2.初始化对象
instance = memory;	 //3.设置引用地址

CPU可能会使2、3语句发生指令重排。如果发生,此时内存已经分配,instance = memory不为null,两次Objects.isNull(dcl)都会跳过,最后返回了对象。但是实际上对象还没初始化。因此程序会出现问题。

你可能感兴趣的:(Java)