voliate工作实际应用场景

哈喽大家好,我是IT老哥,今天我们来讲讲面试必问的voliate

单线程的情况下呢,我们肯定用不到这个voliate

只有在多线程的情景下才能用到,文章结尾我会举一个经典的案例

voliate三特性

  • 保证可见性;

  • 不保证复合操作的原子性;

  • 禁止指令重排。

第一:可见性

先给大家介绍一下JMM的内存模型

        我们定义的共享变量就是存在主内存中,每个线程内的变量是在工作内存中操作的,当一个线程A修改了主内存里的一个共享变量,这个时候线程B是不知道这个值已经修改了,因为线程之间的工作内存是互相不可见的

        那么这个时候voliate的作用就是让A、B线程可以互相感知到对方对共享变量的修改,当线程A更新了共享数据,会将数据刷回到主内存中,而线程B每次去读共享数据时去主内存中读取,这样就保证了线程之间的可见性

这种保证内存可见性的机制是:内存屏障(memory barrier)

内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障有两个作用:

1.阻止屏障两侧的指令重排序;
2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

这里不深入说这个机制了,对于大家目前的情况可能理解起来比较困难

第二:不保证复合操作的原子性

1、什么叫原子性?

所谓原子性,就是说一个操作不可被分割或加塞,要么全部执行,要么全不执行。

i = 0;            ---1
j = i ;           ---2
i++;              ---3
i = j + 1;        ---4

上面四个操作,有哪个几个是原子操作,那几个不是?如果不是很理解,可能会认为都是原子性操作,其实只有1才是原子操作,其余均不是。

  • 1—在Java中,对基本数据类型的变量和赋值操作都是原子性操作;

  • 2—包含了两个操作:读取i,将i值赋值给j

  • 3—包含了三个操作:读取i值、i + 1 、将+1结果赋值给i;

  • 4—同三一样

Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作*,如long、double)

第三:有序性(禁止jvm对代码进行重排序)

有序性:即程序执行的顺序按照代码的先后顺序执行。

一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:

  1. 在单线程环境下不能改变程序运行的结果;

  2. 存在数据依赖关系的不允许重排序

第四:举个非常常见的voliate用法—DCL

什么是DCL呢,其实就是double check lock的简写

DCL很多人都在单利中用过,如下这种写法:

public class Singleton {private static Singleton singleton;private Singleton(){}public static Singleton getInstance(){if(singleton == null){                              // 1synchronized (Singleton.class){                 // 2if(singleton == null){                      // 3singleton = new Singleton();            // 4}}}return singleton;}
}

表面上这个代码看起来很完美,但是其实有问题

先说一下他完美的一面吧:

1、如果检查第一个singleton不为null,则不需要执行下面的加锁动作,极大提高了程序的性能;

2、如果第一个singleton为null,即使有多个线程同一时间判断,但是由于synchronized的存在,只会有一个线程能够创建对象;

3、当第一个获取锁的线程创建完成后singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象;

但是到底是哪里有错误呢,听老哥细细分析

首先创建一个对象分为三个步骤:

1、分配内存空间

2、初始化对象

3、讲内存空间的地址赋值给对象的引用

但是上面我讲了,jvm可能会对代码进行重排序,所以2和3可能会颠倒,

就会变成 1 —> 3 —> 2的过程,

那么当第一个线程A抢到锁执行初始化对象时,发生了代码重排序,3和2颠倒了,这个时候对象对象还没初始化,但是对象的引用已经不为空了,

所以当第二个线程B遇到第一个if判断时不为空,这个时候就会直接返回对象,但此时A线程还没执行完步骤2(初始化对象)。就会造成线程B其实是拿到一个空的对象。造成空指针问题。

解决方案:

既然上面的问题是由于jvm对代码重排序造成的,那我们禁止重排序不就好了吗?

voliate刚好可以禁止重排序,所以改造后的代码如下:

public class Singleton {//通过volatile关键字来确保安全private volatile static Singleton singleton;private Singleton(){}public static Singleton getInstance(){if(singleton == null){synchronized (Singleton.class){if(singleton == null){singleton = new Singleton();}}}return singleton;}
}

这样就不会存在2和3颠倒的问题了

解决方案二:

基于类初始化

该解决方案的根本就在于:利用classloder的机制来保证初始化instance时只有一个线程。JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。

public class Singleton {private static class SingletonHolder{public static Singleton singleton = new Singleton();}public static Singleton getInstance(){return SingletonHolder.singleton;}
}

这种解决方案的实质是:允许步骤2和步骤3重排序,但是不允许其他线程看见。

晚安,兄弟们!

给个[在看],是对IT老哥最大的支持

Published by

风君子

独自遨游何稽首 揭天掀地慰生平