从CPU中缓存架构浅析volatile底层原理

并发编程问题来源
  • 并发编程是每个研发绕不过去的一个难点,并发编程为啥如此的复杂,并发编程为什么回产生可见,有序性,原子性,最终的原因在于,计算机硬件的发展,因为计算机硬件发展太快了,自从出现了多核CPU的架构,以及CPU缓存,才导致这一问题的发生。
    • 具体问题是我们通过CPU来快速执行计算机指令,为了达到处理极致的目的,我们将多个CPU集成到一台主机中,这样可以每个CPU互相不干扰的情况之下执行指令,这样达到了并发的目的
    • 同时,还是为了追求极致的执行速度,每个CPU中都会有自己的一份缓存,这部分缓存用来存储CPU即将要执行的指令需要用到的一些数据,这部分数据会提前将系统内存中数据加载到CPU缓存中
    • 那么问题来了,当两个CPU执行的指令都需要操作同一块内存地址中的数据,就会产生并发问题了,我们怎么保证数据的一致性,以哪个CPU执行的结果为最终结果,这就是并发问题产生的问题之一(这里只列举一个用来说明)
CPU缓存

在这里插入图片描述

  • CPU的运算速度最块,内存的读写速度无法和CPu运算速度匹配。假如定义CPU的一次存储或者访问为一个CPU时钟周期,那么内存的一次运算通常需要几十甚至几百时钟周期。如果在CPU内直接读取内存,那么CPU大部分时间都在等内存的访问,利用率就降低到现在的几十份之一或者几百分之一。为了解决CPU运算速度与内存读写速度不匹配的矛盾,在CPU和内存之间,引入了L1高速缓存,L2高速缓存,L3高速缓存,通过每一级别缓存中所存储的数据,全部都是下一级缓存中的一部分,当CPU直接从缓存中读,提高读写速度,提高CPU利用率,提升整体效率。
    • L1高速缓存:也叫一级缓存,一般在内核旁边,一次访问只需要2~4个时钟周期
    • L2高速缓存:也叫二级缓存,空间比L1缓存大,速度比L1缓存慢,一次访问10多个时钟周期
    • L3高速缓存:也叫三级缓存,部分单CPU多核心的才会有的缓存,介于多核和内存之间,存储空间可达Mb级别,一次访问需要数十个时钟周期。
  • 单CPU需要读取一个数据时,首先从L1缓存查找,命中则返回,接着依次L2,L3,还没有的话直接内存,并且将数据逐级加载到缓存。

在这里插入图片描述

总线锁和缓存锁
  • 操作系统中,我们对volatile关键字编译后,得到的机器语言中会有一个Lock前缀:
    • lock前缀,会保证某个处理器对共享内存(一般是缓存行cacheline,后续介绍)的独占使用。他将本处理器缓存失败,达到了“指令重排序无法越过内存屏障”的作用
  • 总线锁:就是锁住总线。通过处理器发出Lock指令,总线接受到指令后,其他处理器就会被阻塞,直到此处处理器执行完成。这样,处理器就可以肚占共享内存的使用,但是,总线锁存在较大的缺点,当某个处理器获取总线锁,其他处理器只能阻塞等待,多处理器优势就无法发挥了。
  • 经过优化后,又产生了缓存锁:缓存锁就不需要锁总线,只需要被缓存的共享对象(实际就是缓存行)即可,接受到lock指令,通过缓存一致性协议,维护本处理器内部缓存和其他处理器缓存的一致性,相比总线锁,会提告CPU利用率。
缓存行
  • 上面提到的,缓存锁,会锁定共享对象,如果仅仅锁定所用的对象,那么有大有小,随取随用,对于CPU来说,如果每次需要用多个对象,那就会许下一多次加锁释放锁,这样利用率不能最大化。所以采用一次获取整块内存的数据,放入缓存,那么这一块数据,通常称为缓存行(cache line)。缓存行是CPU缓存中可分配,操作的,最小存储单元。与CPU架构有关,有32,64,128字节不等。目前64位架构下64字节最常用
缓存一致性协议
  • 这个之后文章单独写吧,此处只要知道,这个机制可以做到数据一致性,每个处理器的缓存数据和主内存区域
volatile
  • 以上我们主要介绍CPU的缓存,为现在的说明做铺垫
  • Volatile关键字是java虚拟机提供的最轻量级的同步机制,在多线程编程中,volatile和synchronized都起着重要作用。
  • 接下来主要说明volatile的作用,volatile底层原理,以及他和CPU中缓存设计的相关性,可以更好的理解volatile底层实现
volatile的作用
  • 并发编程三大特性:原子性,可见性,有序性

    • 原子性:一个操作或者多个操作集合,要么全部执行 成功/失败。满足原子性操作,中途不可被中断
    • 可见性:多线程共同访问共享变量,某个线程修改此变量,其他线程立即能看到修改后的值。
    • 有序性:程序执行顺序按照代码先后顺序执行。(由于JMM模型允许编译器和处理器为了效率进行指令重新排序的优化。指令重排序再单线程内表现为串行语义,在多线程中会表现出无序。那么多线程并发编程中,就要考虑如何在多线程环境下下可以允许部分指令重排序)
  • synchronized关键字可以同时保证上述三种特性

    • synchronized 是同步锁,同步块内的代码相当于同一时刻单线程执行,不存在原子性和指令冲排序的问题
    • synchronized,关键字的语JMM有两个规定,保证其实现内存可见性
      • 线程解锁前,必须将共享变量的最新值刷新到主内存中
      • 线程加锁前,将清空工作内存中共享变量的值,从主内存中重新取值
  • volatile 关键字的作用是保证可见性和有序性,并不保证原子性

volatile变量的可见性
  • javau 你急规范中定义了一种java内存模型(JMM,Java Memory Model)用来屏蔽各种硬件和操作系统的内存访问差异,以此实现java程序在各种平台下都能达到一致的并发效果。java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的细节。

  • JMM中规定所有变量都存储在主内存(Mail Memory)中。每条线程都有自己的工作内存(work Memory),此处说明的主内存,即上文中的操作系统的内存,每条线程的工作内存,就是我们CPU的高速缓存,从上文对CPU缓存的说明可以看出,线程对变量读,写在工作内存中进行,同时,本线程工作内存的变量无法改变其他线程工作内存,必须通过主内存完成信息交换

  • 内存模型如下:
    在这里插入图片描述

  • 如上图中,线程A修改变量后,只会在此线程工作内存中体现。在为同步到主内存前,如果B也改变了此变量,从主内存中获取到的是修改之前的值,此时就发生了共享变量值不一致,也就是线程可见性问题。

  • volatile定义:

    • 当对volatile变量执行写操作,JMM会吧工作内存中最新变量值强制刷新到主内存
    • 写操作会导致其他线程中的缓存无效。
  • 这样其他线程使用缓存时候,发现本地工作内存中此变量无效,便从主内存中获取,这样获取到的变量便是最新的值,实现了线程的可见性

volatile 有序性保证
  • volatile通过编译器在生成字节码时候,在指令序列中添加“内存屏障”来禁止指令重排序。

  • 硬件方面的“内存屏障”:

    • sfence:写内存屏障(Store Barrier),在写指令之后插入写内存屏障,能让写入缓存的最新数据写回到主内存,以保证写入数据立刻对其他线程可见。
    • ifence:读屏障(Load Barrier)在读指令之前插入读屏障,可以让高速缓存中数据失效,重新从主内存加载数据,以保证读取到是最新的数据
    • mfence:全能屏障(modify/mix Barrier),兼有sfence和ifence
    • lock前缀:lock不是内存屏障,是一种锁,执行时候会锁住主内存子系统来保证顺序执行,甚至跨越多个CPU
  • JMM层面的“内存屏障”:

指令类型指令示例说明
LoadLoad Barriersload1;loadload;load2保证Load1要读取的数据,在load2以及之后的读取操作中要读取的数据被访问前被读取完毕
StoreStore BarriersLoad1;LoadStore;Store2在Store2及后续写入操作执行前,保证Store1的写入操作对其他处理器可见。
LoadStore BarriersLoad1;LoadStore;Store2确保Load1数据的读取操作 先于Load2以及后续写入操作刷新到内存
StoreLoad BarriersStore1;StoreLoad;Load2确保Store1修改的数据对其他处理器的可见性(只刷新到内存)并且这个操作先于Load2以及所有后续装载指令之前完成
  • volatile关键字的内存屏障
    • 为了实现volatile的内存语意,编译器在生产字节码时候,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优解的布置来最小化插入屏障的数量几乎不可能,所以JVM采用保守策略,如下:
    • 在每个volatile写操作的前面加入一个StoreStore Barrier
    • 在每个volatile 写操作后面加入一个StoreLoad Barrier
    • 在每个volatile读彩妆后面插入一个LoadLoad Barrier
    • 在每个volatile读操作后面插入一个LoadStore Barrier
volatile 底层实现
专业术语总结
术语英文描述
内存屏障memory barriers一组计算机处理器指令,用于实现对内存操作的顺序限制
缓存行cache line缓存中可用来分配的最小存储单元。处理器填充缓存时候会加载整个缓存行,西药使用多个主内存读周期
原子操作atomic operations不可中断的一个或者一系列的操作
缓存行填充cache line fill当处理器识别到从内存中读取操作是可缓存的,处理器读取整个缓存行到适当的高速缓存中(L1,L2,L3)
缓存命中cache hit如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器会从缓存中读取操作而不是从内存中读取
写命中write hit当处理器将操作写回到一个内存缓存的区域(即高速缓存)中时候,他首先回检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数据写回到缓存(高速缓存),而不是写回到内存,这个操作被称为写命中
写缺失write misses the cache一个有效的缓存行被写入到不存在的内存区域
volatile 实现原理
instance = new Singletion();
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
  • 以X86处理器下说明,用工具的到java代码编译后的汇编指令来查看在对volatile写操作时候(下一篇说明工具应用,待更新),CPU会怎么处理, 有volatile 修饰的变量在进行写操作的时候,会多出一行汇编代码,就是Lock前缀指令,在多核处理器下会引起两件事情:
    • 将当前处理器缓存行的数据写回到系统内存
    • 这个写回内存的操作会使在其他CPU里缓存了改地址的数据无效(CPU缓存一致性协议)
  • 实现:volatile变量在写操作时候,JVM会向处理器发送一条Lock前缀指令,将这个变量所在的缓存行数据写回到系统内存,然后通过缓存一致性协议通知其他处理器。
volatile两条实现原则
  • Lock前缀指令会引起处理器写回到内存,获取到Lock#信号的处理器可以独占任何共享内存。但是他不锁总线,而是锁缓存,并且写回到内存,接着利用CPU缓存一致性协议确保修改原子性。
  • 一个处理器的缓存写回到内存会导致其他处理器的缓存无效:也就是缓存一致性协议,例如MESI(修改,独占,共享,无效)控制协议去维护内部缓存和其他处理器缓存的一致性
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Volatile是一种Java的关键字,用于标识变量是易变的,即该变量的值可能会在不同的线程发生改变。Volatile底层原理涉及到Java内存模型。 Java内存模型定义了线程如何与内存交互以及线程之间如何共享内存。Java内存模型将内存分为主内存和线程工作内存。主内存是所有线程共享的内存区域,而线程工作内存是每个线程独立拥有的内存区域。 当一个线程访问一个volatile变量时,它会从主内存读取最新的值。而当一个线程更新一个volatile变量时,它会将新的值立即写入主内存。这保证了所有线程对volatile变量的读写操作都是可见的。 此外,volatile还具有禁止指令重排序的作用。在多线程并发编程,编译器为了提高程序执行效率可能会对指令顺序进行重排序,但是这种重排序可能会导致并发问题。使用volatile可以禁止编译器对volatile变量的指令进行重排序,保证了程序的正确性。 总之,volatile底层原理是基于Java内存模型的,它保证了多线程环境下对volatile变量的可见性和禁止指令重排序的特性。 ### 回答2: VolatileJava的关键字之一,用于修饰变量,主要用于多线程编程,以保证线程间变量的可见性和顺序性。 Volatile底层原理主要是通过内存屏障(Memory Barrier)和禁止重排序来实现的。内存屏障是一种CPU指令,能够强制刷新处理器缓存并保证读/写操作顺序的一致性。当一个线程修改了一个被volatile修饰的变量的值时,会立即将该值刷新到主内存,并通知其他线程对对应变量的缓存失效,强制其他线程从主内存重新读取最新值。 此外,volatile还可以禁止指令重排,保证代码的有序执行。在有volatile修饰的变量之前的指令一定会在其后的指令之前执行。这样可以避免了由于指令重排导致的数据不一致问题。 总之,Volatile底层原理主要通过内存屏障以及禁止指令重排来保证线程间变量的可见性和顺序性。它能够确保一个变量在多个线程之间的可见性,尤其用于一个线程修改了变量值时,其他线程能够立即感知到变量的变化,并从主内存重新读取最新值,从而避免了线程间数据不一致的问题。同时,它还通过禁止指令重排,保证了代码的有序执行,避免了由于指令重排导致的逻辑错误。因此,在多线程编程,合理使用Volatile关键字能够确保程序的正确性和稳定性。 ### 回答3: VolatileJava的关键字,用于修饰变量。它的底层原理是通过禁止线程内部的缓存变量副本,直接访问主存的变量值,保证了多线程环境的可见性和有序性。下面详细解释其底层原理。 在多线程环境下,每个线程都有自己的工作内存(线程的私有内存),存放变量的副本。由于性能原因,线程在执行操作时,通常会先将变量从主存读取到工作内存进行操作,然后再将修改的结果写回主存。这种操作称为“读写操作的优化”。 当一个变量被volatile修饰时,它的读写操作会具有特殊的语义。当一个线程对volatile修饰的变量进行写操作时,它会首先将值写入工作内存,然后立即刷新到主存,并且通知其他线程该变量的值已经被修改。而当一个线程对volatile变量进行读操作时,它会立即从主存读取最新的值,并且在读之前使自己的工作内存失效,以保证读操作获取的是最新值。 这种特殊的语义使得volatile能够保证多线程环境下的可见性和有序性。通过禁止线程内部的缓存变量副本,保证了每个线程对volatile变量的读写操作都是基于主存最新的值,从而避免了数据不一致的问题。同时,由于读操作会使工作内存失效,写操作会立即刷新到主存,保证了变量的修改对其他线程的可见性和顺序性。 总结起来,volatile底层原理是通过禁止线程内部的变量副本,直接访问主存的变量值,保证了在多线程环境下的可见性和有序性。它对于一些简单的变量操作可以替代锁,同时也可以用于线程间的通信,但并不能保证原子性。因此,在使用volatile时,需要根据具体的场景和需求来判断是否合适。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值