决战圣地玛丽乔亚Day14 ----Volatile关键字分析

volatile:

背景: 

CPU和主存读写速度不一致,出现了高速缓存。把一些经常读的数据存入高速缓存,交互计算和高速缓存进行,修改完毕后再从高速缓存刷回主存中。

但是问题来了! CPU是多核,不同的cpu都有自己的高速缓存,那么一份数据可能就被缓存在不同的cache中。 并发操作就会造成数据的不准。

硬件方面可以通过缓存一致性协议,Lock锁总线的方式来支持并发操作。但是太过于笨重,是很早期的产物了。然后又出现缓存一致性协议。在写数据时,

判断是否当前使用的数据时共享变量,如果是共享变量就把该变量在其他的CPU缓存中置为失效,如果其他CPU读取发现失效缓存会重新从内存读取。

 

Volatile其实是一个很基础底层的并发问题,如果想完全弄懂,首先要知道并发的三个概念:原子性、可见性、有序性。这是聊并发绕不开的三个概念。

原子性:

要么整个流程全部完成,要么就全部不做。在过程中不可打断。

可见性:

多线程操作,对数据的修改,其他线程是否知情。

有序性:

这个主要是处理器对程序进行效率优化导致,会进行指令重排序。单线程指令重排序,会保证最终的结果一致性,但是如果多线程还是进行指令重排序,就会影响最终结果的一致性。

 

java对于并发三要素的规范:
内存模型(所有变量存在于内存中,每个线程有自己的工作内存(cache))

1.原子性。

对于基础数据类型变量的读写保证原子性。

但是如 X++ 

    x = x +1

           y = x 

这种操作都不是原子性

 

2.可见性

volatile保证,修改volatile的值后会立刻更新到主存

3.有序性

sychronized和Lock保证有序性,因为变成了单线程模式。

java内存模型的happens-before原则:

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对变量的写优先于读
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。  A>B   B>C   A>C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

理解一下就是单线程下,会保证最终结果一致性。变量的写在读之前,变量的解锁在锁之前,变量的顺序有传递性

 

 深入剖析Volatile关键字:

1.保证不同线程的实时可见性!修改完立马刷新数据,其他线程共享的置为失效。

2.禁止指令重排序!

1、原子性

Volatile也只能保证读/写的原子性,对于x++这种,需要先读后写的非一步操作,是不能保证原子性的。

原因:因为修改后立即刷新到内存,其他缓存失效这个操作,是一步到位的。如果是读+写这种非一步操作,完全可以在线程1读后阻塞,然后线程2读,两个线程读到的数据是一致的,

但是两个线程分别+1是在读到的数据上进行修改,读到的数据已经是老数据了,会造成误差。

对于这种情况,我们可以使用sychronized和Lock或者AtomicInteger这种封装好包进行原子性操作。

AtomicInteger主要是用到了CAS操作,CAS说白了就是用处理器提供的CMPXCHG指令实现了原子操作

3、有序性

1)当程序执行到volatile变量的读写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

简单来说,volatile禁止指令重排序,确保了volatile变量修饰的变量的语句在结构上的位置不会发生变化,且其前后的语句的最终一致性要保证。

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5
在这里,  语句1,2内部的顺序可以调换,但是最终的结果要一致。语句3是volatile变量,其位置不会轻易变动。
      语句3,4内部的顺序可以调换,但是不会跑到语句3 的前后
 
volatile的底层原理和实现机制。

前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

  下面这段话摘自《深入理解Java虚拟机》:

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

 

volatile的使用场景:

volatile还是不能完全的取代synchronized,因为他对原子性的支持有限,synchronized能保证完全的原子性。

所以,我们在对状态量的原子性做要求的时候,可以用一用volatile, volatile  boolean  sign = false;

还有一种情况,就是double-check 单例的一种写法,这个也有很多东西可以讲。

public class Single {
    private static Single3 instance;
    private Single() {}
    public static Single getInstance() {
        if (instance == null) {
            synchronized (Single.class) {
                if (instance == null) {
                    instance = new Single3();
                }
            }
        }
        return instance;
    }
}

这个是最初版本的单例模式。
  • 第一个if (instance == null),其实是为了解决效率问题,只有instance为null的时候,才进入synchronized的代码段——大大减少了几率。
  • 第二个if (instance == null),则是为了防止可能出现多个实例的情况

这里看似没有问题,但是问题出在

  instance = new Single3();
这里看似是一个原子操作,实际上底层进行了三步操作:

1.首先给Signle3分配内存空间
2.调用Signle3的构造函数初始化成员变量,形成实例
3.将实例对象指向分配的内存空间

JVM会对这三步进行一个指令重排序,所以2,3的步骤是可能会发生变化的,因为分配内存空间是固定的所以不受影响。
也就是说:有可能先形成实例,然后然后指向内存空间。也有可能先给实例分配内存空间(instance不为null),再进行初始化。
如果把实例对象指向内存空间,这时instance不为null但是需要初始化,线程二突然抢占,线程二不知道这个instance是没有初始化的,直接去进第一个ifnull判断,发现没有问题,不为null,使用后发现没有初始化直接报错。
这里的关键问题在于,线程1没有写,线程2就去读。

所以改进的版本应该是:
public class Single {
    private static volatile Single4 instance;
    private Single() {}
    public static Single getInstance() {
        if (instance == null) {
            synchronized (Single.class) {
                if (instance == null) {
                    instance = new Single();
                }
            }
        }
        return instance;
    }
}
这里就安全了很多,使用了volatile关键字。
  instance = new Single(); 
并不是保证了instance内部三个顺序的禁止指令重排序。
而是说,我在向这个volatile变量instance写入数据完成之前,不允许读。
也就是如果线程2在线程1的new instance之前想要进行ifnull的判断,是不被允许的。
posted @   NobodyHero  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!
点击右上角即可分享
微信分享提示