zoukankan      html  css  js  c++  java
  • java基础---volatile底层实现原理详解

     

    大家都知道生产中可以使用volatile达到保证可见性和指令重排的目的。但是对其实现原理并不是很清楚,为了加深学习和理解感觉很有必要来写篇博客总结一下。

    JMM—java内存模型

    想知道volatile实现原理首先得去了下解JMM,我们都知道JVM会为每一个thread开辟一块自己的工作空间,在我们操作变量时是从主内存拿到变量的一个副本,然后对副本进行操作后再刷新到主内存中这么一个总体的流程。
    在这里插入图片描述
    先简单来看一下如果要改变一个变量值需要经过哪些操作:
    1.首先会执行一个read操作将主内存中的值读取出来
    2.执行load操作将值副本写入到工作内存中
    3.当前线程执行user操作将工作内存中的值拿出在经过执行引擎运算
    4.将运算后的值进行assign操作写会到工作内存。
    5.线程将当前工作内存中新的值存储回主内存,注意只是此处还没有写入到主内存的共享变量,主内存中的值还没有改变。
    6.最后一步执行write操作将主内存中的值刷新到共享变量,到此处一个简单的流程就走完了。

    下图的8种操作是定义在java内存模型当中的,我们的任何操作都需要通过这几种方式来进行。
    在这里插入图片描述
    简单看了一下操作流程后继续回到volatile关键字,在多个个线程工作内存看起来互无关联的情况下是怎么做到保证变量的可见性的?

    这里我们不得不先去了解一个名词:总线 ------什么是总线?它是干什么的?

    度娘给出的解释: 由于总线是连接各个部件的一组信号线。通过信号线上的信号表示信息,通过约定不同信号的先后次序即可约定操作如何实现。简单来说就是我们的cpu和内存进行交互就得通过总线,它们不能隔空产生连接。总线就是一条共享的通信链路,它用一套线路来连接多个子系统。

    总线按功能和规范可分为五大类型:

    • 数据总线(Data Bus):在CPU与RAM之间来回传送需要处理或是需要储存的数据。
    • 地址总线(Address Bus):用来指定在RAM(Random Access Memory)之中储存的数据的地址。
    • 控制总线(Control Bus):将微处理器控制单元(Control Unit)的信号,传送到周边设备。
    • 扩展总线(Expansion Bus):外部设备和计算机主机进行数据通信的总线,例如ISA总线,PCI总线。
    • 局部总线(Local Bus):取代更高速数据传输的扩展总线。

    最初实现就是通过总线加锁的方式也就是上面的lock与unlock操作,但是这种方式存在很大的弊端。会将我们的并行转换为串行,从而失去了多线程的意义。这里不详细展开了解一下即可。下面才是我们真正需要认识的

    MESI缓存一致性协议:

    CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决IO速度和CPU运算速度之间的不匹配问题。为了解决这个问题CPU厂商采用了缓存的解决方案,知道目前我们正在使用的多级的缓存结构。我们可以到任务管理器看一下:
    在这里插入图片描述
    目前流行的多级缓存结构:
    在这里插入图片描述
    多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。这里我们大概只需要有这么一个概念就可以。而当我们共享变量用volatile修饰后就会帮我们在总线开启一个MESI缓存协议。同时会开启CPU总线嗅探机制(监听),当其中一个线程将修改后的值传输到总线时,就会触发总线的监控告诉其他正在使用当前共享变量的线程使当前工作内存中的值失效。这个时候工作空间中的副本变量就没有了,从而需要重新去获取新的值。

    底层实现主要是通过汇编Lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并写回到主内存。总的来说就是Lock指令会将当前处理器缓存行数据立即写回到系统内存从而保证多线程数据缓存的时效性。这个写回内存的操作同时会引起在其它CPU里缓存了该内存地址的数据失效(MESI协议)。

    为了保证在从工作内存刷新回主内存这个阶段主内存数据的安全性,在store前会使用内存模型当中的lock操作来锁定当前主内存中的共享变量。当主内存变量在write操作后才会将当前lock释放掉,别的线程才能继续进来获取新的值。

    查看Java的汇编指令: 想要实际去看下底层的汇编指令,需要在jre/bin目录下添加额外的两个包
    下载链接:百度网盘链接,提取码:d753

    在这里插入图片描述
    然后配置idea,在 VM options 选项中输入:-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*类名.方法名或者-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly就可以在控制台看到输出的lock汇编指令。
    在这里插入图片描述
    我们都知道volatile并不能保证原子性,到这里也可以解释通为什么了。假设当前有两个线程同时对共享变量进行+1运算,Thread1比Thread2先进行了Lock操作拿到了锁,此时由于我们的总线嗅探机制Thread2就会知道共享变量值已经修改过了,从而导致当前Thread2工作内存中的副本变量失效。只能再次去主内存中取新的值,但这样无形之中Thread2就已经浪费掉了一次操作机会。从而导致最终结果小于预期的情况出现。(比如最常用到的那种两个线程同时对一个volatile修饰的int进行加减运算的例子)

    提示:
    如 long a = 100L long b = a+1
    在这里a+1并不是我们想象中的原子操作因为long在java中占8个子节一个64位写操作实际上将会被拆分为2个32位的操作,这一行为的直接后果将会导致最终的结果是不确定的并且缺少原子性的保证。
    在Java虚拟机规范中同样也有类似的描述:“For the purposesof the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each32-bit half. This can result in a situation where a thread sees the first 32 bitsof a 64-bit value from one write, and the second 32 bits from anotherwrite.”

    翻译:对于Java编程语言内存模型来说,对非易失性长值或双值的一次写操作被视为两次单独的写操作:一次写32位的一半。这可能导致这样一种情况,一个线程看到一个64位值的前32位从一个写,和第二个32位从另一个写。

    官网地址

    指令重排

    在之前很经典的单例设计模式中为了防止DCL在指令重排后导致线程不安全的情况,就使用了volatile来防止指令重排。

    我们知道为了提高程序执行的性能,编译器和执行器(处理器)通常会对指令做一些优化(重排序)。volatile通过内存屏障实现了防止指令重排的目的。同时lock前缀指令相当于一个内存屏障,它会告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

    不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。
    Java内存屏障主要有Load和Store两类:

    • 对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据
    • 对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存
      为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。然而,对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略:
    • 在每个volatile写操作的前面插入一个StoreStore屏障。
    • 在每个volatile写操作的后面插入一个StoreLoad屏障。
    • 在每个volatile读操作的后面插入一个LoadLoad屏障。
    • 在每个volatile读操作的后面插入一个LoadStore屏障。
  • 相关阅读:
    fragment+viewpager+tablayou实现滑动切换页面
    XML转义字符
    java非覆盖写入文件及在输出文本中换行
    ObjectInputStream怎么判断是否读到末尾
    Linux下安装jdk(xxx.rpm,非xxx.tar.gz,请注意!)过程
    java接口(interface)
    IDEA快捷键(windows)
    maven插件maven-war-plugin
    maven插件maven-source-plugin
    maven插件maven-assembly-plugin
  • 原文地址:https://www.cnblogs.com/shoshana-kong/p/14106504.html
Copyright © 2011-2022 走看看