精华内容
下载资源
问答
  • 首先强调一点,volatilemesi这两个东西没有半点关系。mesi是缓存一致性的一种实现手段,多核CPU为了保证缓存数据的一致性,通常有两种实现手段,一种是总线锁,另一种是缓存锁。总线锁性能消耗大,缓存锁则一般...

    首先强调一点,volatile和mesi这两个东西没有半点关系。mesi是缓存一致性的一种实现手段,多核CPU为了保证缓存数据的一致性,通常有两种实现手段,一种是总线锁,另一种是缓存锁。总线锁性能消耗大,缓存锁则一般通过缓存一致性来实现。因此我们知道mesi是CPU硬件级别的。 volatile是JAVA的一种关键字,实现了两个功能: 1.可见性 2.禁止乱序。 禁止乱序,在JVM层面使用内存屏障来实现,汇编级别通过lock #指令来实现。

    问题:既然CPU有了MESI协议可以保证cache的一致性,那么为什么还需要volatile这个关键词来保证可见性(内存屏障)?或者是只有加了volatile的变量在多核cpu执行的时候才会触发缓存一致性协议?

     

    两个解释结论:

    多核情况下,所有的cpu操作都会涉及缓存一致性的校验,只不过该协议是弱一致性,不能保证一个线程修改变量后,其他线程立马可见,也就是说虽然其他CPU状态已经置为无效,但是当前CPU可能将数据修改之后又去做其他事情,没有来得及将修改后的变量刷新回主存,而如果此时其他CPU需要使用该变量,则又会从主存中读取到旧的值。而volatile则可以保证可见性,即立即刷新回主存,修改操作和写回操作必须是一个原子操作;
    正常情况下,系统操作并不会进行缓存一致性的校验,只有变量被volatile修饰了,该变量所在的缓存行才被赋予缓存一致性的校验功能。

    这个解释仅供参考。。。。。。。。。。。。。。。。。

     

    我们再来看一下另一位大N的解释:

    首先,volatile是java语言层面给出的保证,MSEI协议是多核cpu保证cache一致性(后面会细说这个一致性)的一种方法,中间隔的还很远,我们可以先来做几个假设:

    1. 回到远古时候,那个时候cpu只有单核,或者是多核但是保证sequence consistency[1],当然也无所谓有没有MESI协议了。那这个时候,我们需要java语言层面的volatile的支持吗?当然是需要的,因为在语言层面编译器和虚拟机为了做性能优化,可能会存在指令重排的可能,而volatile给我们提供了一种能力,我们可以告诉编译器,什么可以重排,什么不可以。
    2. 那好,假设更进一步,假设java语言层面不会对指令做任何的优化重排,那在多核cpu的场景下,我们还需要volatile关键字吗?答案仍然是需要的。因为 MESI只是保证了多核cpu的独占cache之间的一致性,但是cpu的并不是直接把数据写入L1 cache的,中间还可能有store buffer。有些arm和power架构的cpu还可能有load buffer或者invalid queue等等。因此,有MESI协议远远不够。
    3. 再接着,让我们再做一个更大胆的假设。假设cpu中这类store buffer/invalid queue等等都不存在了,cpu是数据是直接写入cache的,读取也是直接从cache读的,那还需要volatile关键字吗?你猜的没错,还需要的。原因就在这个“一致性”上。consistency和coherence都可以被翻译为一致性,但是MSEI协议这里保证的仅仅coherence而不是consistency。那consistency和cohence有什么区别呢?下面取自wiki[2]的一段话:
    Coherence deals with maintaining a global order in which writes to a single location or single variable are seen by all processors. Consistency deals with the ordering of operations to multiple locations with respect to all processors.

    因此,MESI协议最多只是保证了对于一个变量,在多个核上的读写顺序,对于多个变量而言是没有任何保证的。很遗憾,还是需要volatile~~

    4. 好的,到了现在这步,我们再来做最后一个假设,假设cpu写cache都是按照指令顺序fifo写的,那现在可以抛弃volatile了吧?你觉得呢?我都写到标题4了,那肯定不行啊!因为对于arm和power这个weak consistency[3]的架构的cpu来说,它们只会保证指令之间有比如控制依赖,数据依赖,地址依赖等等依赖关系的指令间提交的先后顺序,而对于完全没有依赖关系的指令,比如x=1;y=2,它们是不会保证执行提交的顺序的,除非你使用了volatile,java把volatile编译成arm和power能够识别的barrier指令,这个时候才是按顺序的。

    最后总结上文,答案就是:还需要~~

    引用:https://www.zhihu.com/question/296949412/answer/747494794

    展开全文
  • 结论:MESI与JMM无关,更与volatile无关,volatile只是实现JMM的一部分 JMM是一种虚的概念,目的是实现java跨平台。其中,volatile可以解决JMM中存在的一些问题,但不能解决所有问题,比如不能解决原则性。但是可以...

    1. 前言

    看的知识多了,有点混淆,特此把相关的概念梳理一下,做个区分

    2. 结论

    结论:MESI与JMM无关,更与volatile无关,volatile只是实现JMM的一部分

    JMM是一种虚的概念,目的是实现java跨平台。其中,volatile可以解决JMM中存在的一些问题,但不能解决所有问题,比如不能解决原则性。但是可以解决重排序和可见性。

    MESI是硬件层面的,为了解决cpu与缓存之间的数据一致性问题。

    3. java内存模型

    java内存模型,英文全称为java memory model,简称JMM。Java内存模型是一种抽象的概念,并不真实存在,内存模型描述了java多线程与共享内存的交互过程。更进一步说,java内存模型描述了共享内存中的值被多线程修改和读取的过程,规定了共享变量的可见性和执行顺序。

    JMM概念中存在工作内存(线程私有)和主内存(共享内存)。

    Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行。

    java虚拟机规范中定义了Java内存模型(java Memory Model,JMM),用来屏蔽掉各种硬件和操作系统访问内存的差异(内存访问差异),来实现java程序在各种平台下都能达到一致的并发效果,一次编写,到处运行。

    3.1 JMM的三个特征

    Java内存模型是围绕着并发编程中原子性、可见性、有序性这三个特征来建立的,那我们依次看一下这三个特征:

    • 原子性(Atomicity)
      一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。

      基本类型数据的访问大都是原子操作,long 和double类型的变量是64位,但是在32位JVM中,32位的JVM会将64位数据的读写操作分为2次32位的读写操作来进行,这就导致了long、double类型的变量在32位虚拟机中是非原子操作,数据有可能会被破坏,也就意味着多个线程在并发访问的时候是线程非安全的

      存在线程非安全问题,只能通过加锁的形式解决。volatile解决不了原子性。

    • 可见性:
      一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量的这种修改(变化)。

      Java内存模型是通过将在工作内存中的变量修改后的值同步到主内存,在读取变量前从主内存刷新最新值到工作内存中,这种依赖主内存的方式来实现可见性的。由于这种方式同步,导致可能延迟的情况。

      无论是普通变量还是volatile变量都是如此,区别在于:volatile的特殊规则保证了volatile变量值修改后的新值立刻同步到主内存,每次使用volatile变量前立即从主内存中刷新,因此volatile保证了多线程之间的操作变量的可见性,而普通变量则不能保证这一点。

      volatile可以解决可见性

      除了volatile关键字能实现可见性之外,还有synchronized,Lock,final也是可以的

    • 有序性:
      对于一个线程的代码而言,我们总是以为代码的执行是从前往后的,依次执行的。这么说不能说完全不对,在单线程程序里,确实会这样执行;但是在多线程并发时,程序的执行就有可能出现乱序。用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。

      volatile可以解决有序性,会强制避免指令重排

    4. MESI

    MESI是硬件层面的知识。

    线程是CPU调度的最小单元,线程设计的目的最终仍然是更充分的利用计算机处理的效能,但是绝大部分的运算任务不能只依靠处理器“计算”就能完成,处理器还需要与内存交互,比如读取运算数据、存储运算结果,这个 I/O 操作是很难消除的。而由于计算机的存储设备与处理器的运算速度差距非常大,所以现代计算机系统都会增加一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存和处理器之间的缓冲:将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步到内存之中。

    查看我们个人电脑的配置可以看到,CPU有L1,L2,L3三级缓存,大致粗略的结构如下图所示:
    在这里插入图片描述
    从上图可以知道,L1和L2缓存为各个CPU独有,而有了高速缓存的存在以后,每个 CPU 的处理过程是,先将计算需要用到的数据缓存在 CPU 高速缓存中,在 CPU进行计算时,直接从高速缓存中读取数据并且在计算完成之后写入到缓存中。在整个运算过程完成后,再把缓存中的数据同步到主内存。
    由于在多 CPU 中,每个线程可能会运行在不同的 CPU 内,并且每个线程拥有自己的高速缓存。同一份数据可能会被缓存到多个 CPU 中,如果在不同 CPU 中运行的不同线程看到同一份内存的缓存值不一样就会存在缓存不一致的问题,那么怎么解决缓存一致性问题呢

    CPU层面提供了两种解决方法:总线锁和缓存锁

    • 总线锁
      总线锁,简单来说就是,在多CPU下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个 LOCK#信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把 CPU 和内存之间的通信锁住了(CPU和内存之间通过总线进行通讯),这使得锁定期间,其他处理器不能操作其他内存地址的数据。然而这种做法的代价显然太大,那么如何优化呢?优化的办法就是降低锁的粒度,所以CPU就引入了缓存锁。

      该方案效率太低了

    • 缓存锁
      缓存锁的核心机制是基于缓存一致性协议来实现的,一个处理器的缓存回写到内存会导致其他处理器的缓存无效,IA-32处理器和Intel 64处理器使用MESI实现缓存一致性协议(注意,缓存一致性协议不仅仅是通过MESI实现的,不同处理器实现了不同的缓存一致性协议)

      该方案效率高!MESI是该方案的有力支持。

    展开全文
  • 作者公众号:一角钱技术(org_yijiaoqian)前言并发编程从操作系统底层工作的整体认识开始深入理解Java内存模型(JMM)及volatile关键字前面我们从操作系统底层了解了现代计算机结构模型中的CPU指令结构、CPU缓存结构、...
    76128d593edb5efca2501bad6f4ec34a.png

    作者公众号:一角钱技术(org_yijiaoqian)

    前言

    • 并发编程从操作系统底层工作的整体认识开始
    • 深入理解Java内存模型(JMM)及volatile关键字

    前面我们从操作系统底层了解了现代计算机结构模型中的CPU指令结构、CPU缓存结构、CPU运行调度以及操作系统内存管理,并且学习了Java内存模型(JMM)和 volatile 关键字的一些特性。本篇来深入理解CPU缓存一致性协议(MESI),最后来讨论既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?

    CPU高速缓存(Cache Memory)

    CPU为何要有高速缓存

    CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决IO速度和CPU运算速度之间的不匹配问题。

    在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。

    时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。

    比如循环、递归、方法的反复调用等。

    空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。

    比如顺序执行的代码、连续创建的两个对象、数组等。

    带有高速缓存的CPU执行计算的流程

    1. 程序以及数据被加载到主内存
    2. 指令和数据被加载到CPU的高速缓存
    3. CPU执行指令,把结果写到高速缓存
    4. 高速缓存中的数据写回主内存
    37491eb3984ae2f76eeb3c10cc72df07.png

    目前流行的多级缓存结构

    由于CPU的运算速度超越了1级缓存的数据I/O能力,CPU厂商又引入了多级的缓存结构。多级缓存结构示意图如下:

    f04479eea22943c0bca9b56c8c41dad5.png

    多核CPU多级缓存一致性协议MESI

    多核CPU的情况下有多个一级缓存,如果保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI

    MESI 协议缓存状态

    MESI 是指4个状态的首字母。每个 Cache line 有4个状态,可用2个bit表示,它们分别是:

    缓存行(Cache line):缓存存储数据的单元

    49b40566d758fb116b8bccba8c845315.png

    注意:对于M 和 E 状态而言是精确的,它们在和该缓存行的真正状态是一致的,而 S 状态可能是非一致的。

    如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将缓存行升迁为E状态,这是因为其他缓存不会广播它们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。

    从上面的意义来看 E状态 是一种投机性的优化:如果一个CPU想修改一个处于 S状态 的缓存行,总线事物需要将所有该缓存行的 copy 变成 invalid 状态,而修改 E状态 的缓存不需要使用总线事物。

    MESI 状态转换

    78fe23dd689d8a18922c4e759bff85f4.png

    理解该图的前置说明:

    1. 触发事件
    8334b9357c839fff6ab80cc9eb16b365.png
    1. cache分类
    • 前提:所有的cache共同缓存了主内存中的某一条数据。
    • 本地cache:指当前cpu的cache。
    • 触发cache:触发读写事件的cache。
    • 其他cache:指既除了以上两种之外的cache。
    • 注意:本地的事件触发 本地cache和触发cache为相同。

    上图的切换解释:

    240cab9b314e11b53e6a80f115e970ef.png

    下图示意了,当一个cache line的调整的状态的时候,另外一个cache line 需要调整的状态。

    60ed496cdcb7891377520d6a4a3e87a7.png

    举例子来说:假设 cache 1 中有一个变量 x = 0 的 cache line 处于 S状态(共享)。 那么其他拥有 x 变量的 cache 2 、cache 3 等 x 的cache line 调整为 S状态(共享)或者调整为 I状态(无效)。

    多核缓存协同操作

    假设有三个CPU A、B、C,对应三个缓存分别是cache a、b、 c。在主内存中定义了x的引用值为0。

    1da57151ec78edb36d4620c7a43616a5.png

    单核读取

    那么执行流程是: CPU A 发出了一条指令,从主内存中读取x。

    从主内存通过bus读取到缓存中(远端读取Remote read),这是该 Cache line 修改为 E状态(独享).

    cc9c20fc30de89ac22a8d7b8c426d097.png

    双核读取

    那么执行流程是:

    • CPU A 发出了一条指令,从主内存中读取x。
    • CPU A 从主内存通过bus读取到 cache a 中并将该 cache line 设置为 E状态
    • CPU B 发出了一条指令,从主内存中读取x。
    • CPU B 试图从主内存中读取x时,CPU A 检测到了地址冲突。这时CPU A对相关数据做出响应。此时x 存储于cache a和cache b中,x在chche a和cache b中都被设置为S状态(共享)。
    4784953f06253d70c06914daf13763e9.png

    修改数据

    那么执行流程是:

    • CPU A 计算完成后发指令需要修改x.
    • CPU A 将x设置为 M状态(修改)并通知缓存了x的CPU B, CPU B将本地cache b中的x设置为 I状态(无效)
    • CPU A 对x进行赋值。
    ddab41ca013459c5f17ee2b5ca102193.png

    同步数据

    那么执行流程是:

    • CPU B 发出了要读取x的指令。
    • CPU B 通知 CPU A,CPU A将修改后的数据同步到主内存时 cache a 修改为 E(独享)
    • CPU A同步CPU B的x,将cache a和同步后cache b中的x设置为 S状态(共享)
    567470028c08ebbf66446940337180d1.png

    缓存行伪共享

    什么是伪共享?

    CPU缓存系统中是以缓存行(cache line)为单位存储的。目前主流的CPU Cache 的 Cache Line 大小都是64Bytes。在多线程情况下,如果需要修改“共享同一个缓存行的变量”,就会无意中影响彼此的性能,这就是伪共享(False Sharing)。

    举个例子: 现在有2个long 型变量 a 、b,如果有t1在访问a,t2在访问b,而a与b刚好在同一个cache line中,此时t1先修改a,将导致b被刷新!

    怎么解决伪共享?

    Java8中新增了一个注解: @sun.misc.Contended 。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置 -XX:-RestrictContended 才会生效。

    @sun.misc.Contendedpublic final static class VolatileLong {    public volatile long value = 0L;    //public long p1, p2, p3, p4, p5, p6;}

    MESI优化和他们引入的问题

    缓存的一致性消息传递是要时间的,这就使其切换时会产生延迟。当一个缓存被切换状态时其他缓存收到消息完成各自的切换并且发出回应消息这么一长串的时间中CPU都会等待所有缓存响应完成。可能出现的阻塞都会导致各种各样的性能问题和稳定性问题。

    CPU切换状态阻塞解决-存储缓存(Store Bufferes)

    比如你需要修改本地缓存中的一条信息,那么你必须将 **I(无效)状态 **通知到其他拥有该缓存数据的CPU缓存中,并且等待确认。等待确认的过程会阻塞处理器,这会降低处理器的性能。因为这个等待远远比一个指令的执行时间长得多。

    Store Bufferes

    为了避免这种CPU运算能力的浪费,**Store Bufferes** 被引入使用。处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。但这么做有两个风险。

    Store Bufferes的风险

    • 第一:就是处理器会尝试从存储缓存(Store buffer)中读取值,但它还没有进行提交。这个的解决方案称为 Store Forwarding,它使得加载的时候,如果存储缓存中存在,则进行返回。
    • 第二:保存什么时候会完成,这个并没有任何保证。
    value = 3;void exeToCPUA(){  value = 10;  isFinsh = true;}void exeToCPUB(){  if(isFinsh){    //value一定等于10?!    assert value == 10;  }}

    试想一下开始执行时,CPU A 保存着 isFinsh 在 E(独享)状态,而 value 并没有保存在它的缓存中。(例如,Invalid)。在这种情况下,value 会比 isFinsh 更迟地抛弃存储缓存。完全有可能 CPU B 读取 isFinsh 的值为true,而value的值不等于10。即isFinsh的赋值在value赋值之前

    这种在可识别的行为中发生的变化称为重排序(reordings)。注意,这不意味着你的指令的位置被恶意(或者好意)地更改。 它只是意味着其他的CPU会读到跟程序中写入的顺序不一样的结果。

    硬件内存模型

    执行失效也不是一个简单的操作,它需要处理器去处理。另外,存储缓存(Store Buffers)并不是无穷大的,所以处理器有时需要等待失效确认的返回。这两个操作都会使得性能大幅降低。为了应付这种情况,引入了失效队列(invalid queue)。它们的约定如下:

    • 对于所有的收到的Invalidate请求,Invalidate Acknowlege消息必须立刻发送
    • Invalidate并不真正执行,而是被放在一个特殊的队列中,在方便的时候才会去执行。
    • 处理器不会发送任何消息给所处理的缓存条目,直到它处理Invalidate。

    即便是这样处理器已然不知道什么时候优化是允许的,而什么时候并不允许。

    干脆处理器将这个任务丢给了写代码的人。这就是内存屏障(Memory Barriers)。

    • 写屏障 Store Memory Barrier(a.k.a. ST, SMB, smp_wmb)是一条告诉处理器在执行这之后的指令之前,应用所有已经在存储缓存(store buffer)中的保存的指令。
    • 读屏障Load Memory Barrier (a.k.a. LD, RMB, smp_rmb)是一条告诉处理器在执行任何的加载前,先应用所有已经在失效队列中的失效操作的指令。
    void executedOnCpu0() {    value = 10;    //在更新数据之前必须将所有存储缓存(store buffer)中的指令执行完毕。    storeMemoryBarrier();    finished = true;}void executedOnCpu1() {    while(!finished);    //在读取之前将所有失效队列中关于该数据的指令执行完毕。    loadMemoryBarrier();    assert value == 10;}

    总结

    既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?

    volatile是java语言层面给出的保证,MSEI协议是多核cpu保证cache一致性的一种方法,中间隔得还很远,我们可以先来做几个假设:

    1. 回到远古时候,那个时候cpu只有单核,或者是多核但是保证sequence consistency,当然也无所谓有没有MESI协议了。那这个时候,我们需要java语言层面的volatile的支持吗?

    当然是需要的,因为在语言层面编译器和虚拟机为了做性能优化,可能会存在指令重排的可能,而volatile给我们提供了一种能力,我们可以告诉编译器,什么可以重排,什么不可以。

    1. 那好,假设更进一步,假设java语言层面不会对指令做任何的优化重排,那在多核cpu的场景下,我们还需要volatile关键字吗?

    答案仍然是需要的。因为 MESI只是保证了多核cpu的独占cache之间的一致性,但是cpu的并不是直接把数据写入L1 cache的,中间还可能有store buffer。有些arm和power架构的cpu还可能有load buffer或者invalid queue等等。因此,有MESI协议远远不够。

    1. 再接着,让我们再做一个更大胆的假设。假设cpu中这类store buffer/invalid queue等等都不存在了,cpu是数据是直接写入cache的,读取也是直接从cache读的,那还需要volatile关键字吗?

    你猜得没错,还需要的。原因就在这个“一致性”上。consistency和coherence都可以被翻译为一致性,但是MSEI协议这里保证的仅仅coherence而不是consistency。那consistency和cohence有什么区别呢?

    下面取自wiki的一段话: Coherence deals with maintaining a global order in which writes to a single location or single variable are seen by all processors. Consistency deals with the ordering of operations to multiple locations with respect to all processors.

    因此,MESI协议最多只是保证了对于一个变量,在多个核上的读写顺序,对于多个变量而言是没有任何保证的。很遗憾,还是需要volatile~~

    1. 好的,到了现在这步,我们再来做最后一个假设,假设cpu写cache都是按照指令顺序fifo写的,那现在可以抛弃volatile了吧?你觉得呢?

    那肯定不行啊!因为对于arm和power这个weak consistency的架构的cpu来说,它们只会保证指令之间有比如控制依赖,数据依赖,地址依赖等等依赖关系的指令间提交的先后顺序,而对于完全没有依赖关系的指令,比如x=1;y=2,它们是不会保证执行提交的顺序的,除非你使用了volatile,java把volatile编译成arm和power能够识别的barrier指令,这个时候才是按顺序的。

    最后总结,答案就是:还需要~~

    参考资料

    • [1] http://igoro.com/archive/gallery-of-processor-cache-effects/
    • [2] https://en.wikipedia.org/wiki/Sequential_consistency
    • [3] https://en.wikipedia.org/wiki/Consistency_model
    • [4] Maranget, Luc, Susmit Sarkar, and Peter Sewell. "A tutorial introduction to the ARM and POWER relaxed memory models." Draft available from http://www. cl. cam. ac. uk/~ pes20/ppc-supplemental/test7. pdf (2012).
    • [5] https://www.zhihu.com/question/296949412?sort=created

    PS:以上代码提交在 Github

    https://github.com/Niuh-Study/niuh-juc-final.git

    文章持续更新,可以公众号搜一搜「 一角钱技术 」第一时间阅读, 本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。

    展开全文
  • 2.CAS指令,确保了对同一个同一个内存地址操作的原子性,那么他应该也会遇到和上面可见性一样的问题,他是怎么解决的,是不是和volatile的底层原理类似?--->是的,也是利用了MESI 3.volatile还避免了指令重排...

    最近又看了下Disruptor,里面提到了内存屏障,突然想到了指令重排、还有可见性,感觉里面关系有点乱,就翻了下,因此就写了这篇文章

    带着几个问题:

    • 1.volatile,是怎么可见性的问题(CPU缓存),那么他是怎么解决的--->MESI
    • 2.CAS指令,确保了对同一个同一个内存地址操作的原子性,那么他应该也会遇到和上面可见性一样的问题,他是怎么解决的,是不是和volatile的底层原理类似?--->是的,也是利用了MESI
    • 3.volatile还避免了指令重排,是通过内存屏障解决的?那么他和MESI有什么关系?还是说volatile关键字即用了MESI也用了内存屏障?--->是的,其实MESI底层也还是需要内存屏障

    一、可见性和MESI

    1.1 可见性

    在JVM的内存模型中,每个线程有自己的工作内存,实际上JAVA线程借助了底层操作系统线程实现,一个JVM线程对应一个操作系统线程,线程的工作内存其实是cpu寄存器和高速缓存的抽象

    现代处理器的缓存一般分为三级,由每一个核心独享的L1、L2 Cache,以及所有的核心共享L3 Cache组成,具体每个cache,实际上是有很多缓存行组成:

     

    1.2 缓存一致性和MESI

    缓存一致性协议给缓存行(通常为64字节)定义了个状态:独占(exclusive)、共享(share)、修改(modified)、失效(invalid),用来描述该缓存行是否被多处理器共享、是否修改。所以缓存一致性协议也称MESI协议。

    • 独占(exclusive):仅当前处理器拥有该缓存行,并且没有修改过,是最新的值。
    • 共享(share):有多个处理器拥有该缓存行,每个处理器都没有修改过缓存,是最新的值。
    • 修改(modified):仅当前处理器拥有该缓存行,并且缓存行被修改过了,一定时间内会写回主存,会写成功状态会变为S。
    • 失效(invalid):缓存行被其他处理器修改过,该值不是最新的值,需要读取主存上最新的值。

    协议协作如下:

    • 一个处于M状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。
    • 一个处于S状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。
    • 一个处于E状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。
    • 当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取,并把自己状态变成S,如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。
    • 当CPU需要写数据时,只有在其缓存行是M或者E的时候才能执行,否则需要发出特殊的RFO指令(Read Or Ownership,这是一种总线事务),通知其他CPU置缓存无效(I),这种情况下会性能开销是相对较大的。在写入完成后,修改其缓存状态为M。

    这个图的含义就是当一个core持有一个cacheline的状态为Y时,其它core对应的cacheline应该处于状态X, 比如地址 0x00010000 对应的cacheline在core0上为状态M, 则其它所有的core对应于0x00010000的cacheline都必须为I , 0x00010000 对应的cacheline在core0上为状态S, 则其它所有的core对应于0x00010000的cacheline 可以是S或者I ,

    另外MESI协议为了提高性能,引入了Store Buffe和Invalidate Queues,还是有可能会引起缓存不一致,还会再引入内存屏障来确保一致性,可以参考[7]和[12]

    存储缓存(Store Buffe)

    也就是常说的写缓存,当处理器修改缓存时,把新值放到存储缓存中,处理器就可以去干别的事了,把剩下的事交给存储缓存。

    失效队列(Invalidate Queues)

    处理失效的缓存也不是简单的,需要读取主存。并且存储缓存也不是无限大的,那么当存储缓存满的时候,处理器还是要等待失效响应的。为了解决上面两个问题,引进了失效队列(invalidate queue)。处理失效的工作如下:

    • 收到失效消息时,放到失效队列中去。
    • 为了不让处理器久等失效响应,收到失效消息需要马上回复失效响应。
    • 为了不频繁阻塞处理器,不会马上读主存以及设置缓存为invlid,合适的时候再一块处理失效队列。

    1.3 MESI和CAS关系

    在x86架构上,CAS被翻译为”lock cmpxchg...“,当两个core同时执行针对同一地址的CAS指令时,其实他们是在试图修改每个core自己持有的Cache line,

    假设两个core都持有相同地址对应cacheline,且各自cacheline 状态为S, 这时如果要想成功修改,就首先需要把S转为E或者M, 则需要向其它core invalidate 这个地址的cacheline,则两个core都会向ring bus发出 invalidate这个操作, 那么在ringbus上就会根据特定的设计协议仲裁是core0,还是core1能赢得这个invalidate, 胜者完成操作, 失败者需要接受结果, invalidate自己对应的cacheline,再读取胜者修改后的值, 回到起点.

    对于我们的CAS操作来说, 其实锁并没有消失,只是转嫁到了ring bus的总线仲裁协议中. 而且大量的多核同时针对一个地址的CAS操作会引起反复的互相invalidate 同一cacheline, 造成pingpong效应, 同样会降低性能(参考[9])。当然如果真的有性能问题,我觉得这可能会在ns级别体现了,一般的应用程序中使用CAS应该不会引起性能问题

    二、指令重排和内存屏障

    2.1 指令重排

    现代CPU的速度越来越快,为了充分的利用CPU,在编译器和CPU执行期,都可能对指令重排。举个例子:

     

    LDR R1, [R0];//操作1
    ADD R2, R1, R1;//操作2
    ADD R3, R4, R4;//操作3
    

    上面这段代码,如果操作1如果发生cache miss,则需要等待读取内存外存。看看有没有能优先执行的指令,操作2依赖于操作1,不能被优先执行,操作3不依赖1和2,所以能优先执行操作3。
    JVM的JSR-133规范中定义了as-if-serial语义,即compiler, runtime, and hardware三者需要保证在单线程模型下程序不会感知到指令重排的影响。

    在并发模型下,重排序还是可能会引发问题,比较经典的就是“单例模式失效”问题(DoubleCheckedLocking):

     

    public class Singleton {
      private static Singleton instance = null;
    
      private Singleton() { }
    
      public static Singleton getInstance() {
         if(instance == null) {
            synchronzied(Singleton.class) {
               if(instance == null) {
                   instance = new Singleton();  //
               }
            }
         }
         return instance;
       }
    }
    

    上面这段代码,初看没问题,但是在并发模型下,可能会出错,那是因为instance= new Singleton()并非一个原子操作,它实际上下面这三个操作:

     

    memory =allocate();    //1:分配对象的内存空间
    ctorInstance(memory);  //2:初始化对象
    instance =memory;     //3:设置instance指向刚分配的内存地址
    

    上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:

     

    memory =allocate();    //1:分配对象的内存空间
    instance =memory;     //3:instance指向刚分配的内存地址,此时对象还未初始化
    ctorInstance(memory);  //2:初始化对象
    

    可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。在多线程场景下,可能A线程执行到了3,B线程发现已经不为空就返回继续执行,就会出错。

    在java里面volatile可以防止重排,当然还有另外一个作用即内存可见性,这个知道的人还应该比较普遍,就不说了

    2.2 内存屏障

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

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

    在JSR规范中定义了4种内存屏障:

    • LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
    • LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
    • StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
    • StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

    对于volatile关键字,按照规范会有下面的操作:

    • 在每个volatile写入之前,插入一个StoreStore,写入之后,插入一个StoreLoad
    • 在每个volatile读取之前,插入LoadLoad,之后插入LoadStore

    具体到X86来看,其实没那么多指令,只有StoreLoad:

     

    结合上面的【一】和【二】的内容,内存屏障首先阻止了指令的重排,另外也和MESI协议结合,确保了内存的可见性

    三、happends-before

    结合前面的两点,再看happends-before就比较好理解了。因为光说可见性和重排很难联想到happends-before。这个点在并发编程里还是非常重要的,再详细记录下:

    • 1.Each action in a thread happens-before every subsequent action in that thread
    • 2.An unlock on a monitor happens-before every subsequent lock on that monitor.
    • 3.A write to a volatile field happens-before every subsequent read of that volatile
    • 4.A call to start() on a thread happens-before any actions in the started thread.
    • 5.All actions in a thread happen-before any other thread successfully returns from a join() on
      that thread.
    • 6.If an action a happens-before an action b, and b happens before an action c, then a happensbefore c

    四、实现 --> #lock

    再往下挖一层,会发现volatile关键字,转换成指令以后,会有一个#lock前缀...原来以为会有相应的内存屏障指令,说好的内存屏障的那些呢?
    后来参考了资料[11]以及其他一些文章以后才了解到,任何带有lock前缀的指令以及CPUID等指令都有内存屏障的作用。

    参考





     

    展开全文
  • volatile关键字有两个作用保证被volatile修饰的共享变量(vlatile int a=1)对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知(数据的可见性)。...
  • volatile和Cache一致性协议之MESI

    万次阅读 热门讨论 2017-08-14 17:46:06
    volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以重获生机。  volatile关键字...
  • 说了很多,我们最后再回到最初的例子:volatile关键字实现了MESI协议,保证了多CPU(或多线程)对变量修改符合缓存一致性原则,加上volatile关键字后,确保了多线程对变量修改的可见性,在一处修改变量后,另一处...
  • MESI协议虽然可以实现缓存的一致性,但是也会存在一些问题。 就是各个CPU缓存行的状态是通过消息传递来进行的。如果CPU0要对一个在缓存中共享的变量进行写入,首先需要发送一个失效的消息给到其他缓存了该数据的CPU...
  • 缓存带来的问题三、MESI缓存一致性协议篇1,MESI协议2,MESI协议带来的问题四、处理器指令重排篇1,指令重排的实现2,指令重排带来的问题---可见性问题五、内存屏障1,内存屏障2,内存屏障的问题六、Volatile对可见...
  • mesi协议是Intel为了缓存一致性 (内存,cpu高速缓存)两线程同时读取内存内的值,然后进行+1操作,但是最后只加了1 解决 1.加锁 它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也...
  • 加了volatile后 会增加一条总线,当数据改变之后通过store、write写回主内存过程中,总线会监听到这个值发生改变,会通知其他线程把副本中的数据删掉,要使用这个数据时重新从主内存中拿。 写回主内存(堆)去了(锁住...
  • MESI协议 MESI协议是一个被广泛使用的CPU缓存一致性协议。我们都知道在CPU中存在着多级缓存,缓存级别越低,容量就越小,速度也越快。有了缓存,CPU就不需要每次都向主存读写数据,这提高了CPU的运行速度。然而,在...
  • volatile底层实现(CPU的缓存一致性协议MESI)

    千次阅读 热门讨论 2019-07-20 10:59:59
    CPU的缓存一致性协议MESI 在多核CPU中,内存中的数据会在多个核心中存在数据副本,某一个核心发生修改操作,就产生了数据不一致的问题,而一致性协议正是用于保证多个CPU cache之间缓存共享数据的一致性。 cache的写...
  • 初识volatile Java语言规范第3版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。 这个概念听起来有些抽象,我们先看...
  • 本文和后续文章将着眼CPU的工作原理阐述伪共享的解决方法和volatile关键字的应用。
  • volatile实现原理分析
  • http://www.infoq.com/cn/articles/ftf-java-volatile ...volatile原理 底层是靠一个lock指令来保证顺序性和 可见性 1.lock指令会引起处理器中的工作缓存的数据强制写回到工作内存 2.lock指令会令其它CPU的工作...
  • 前面我们从操作系统底层了解了现代计算机结构模型中的CPU指令结构、CPU缓存结构、CPU运行调度以及操作系统内存管理,并且学习了Java内存模型(JMM)和 volatile 关键字的一些特性。本篇来深入理解CPU缓存一致性协议(..
  • happens-before规则 对某个volatile字段的写操作happens-before每个后续对该volatile字段的读操作 特性: 可见性: 对任意单个volatile变量的读,总是能看到(任意线程)对该volatile最后的写入 原子性: 对任意单个...
  • 1.MESI 概念 MESI(Modified Exclusive Shared Or Invalid)(也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出)是一种广泛使用的支持写回策略的缓存一致性协议。 MESI协议中的状态:CPU中每个缓存行...
  • 3. volatile应用场景 3 4. 内存屏障(Memory Barrier) 4 5. setjmp和longjmp 4 1) 结果1(非优化编译:g++ -g -o x x.cpp -O0) 5 2) 结果2(优化编译:g++ -g -o x x.cpp -O2) 6 6. 不同CPU架构的一致性模型...
  • 缓存一致性协议和 java 的 volatile 实现无任何关系 cache line的概念 缓存行对齐 伪共享 a. cache是以cache line为单位与内存映射的,cache只要命中了就是命中64字节,对应内存中连续的64个地址,这64个地址刚好...
  • Volatile

    2021-04-12 20:38:06
    Volatile先了解一波计算机内存模型MESI(缓存一致性协议) 可见性怎么发现数据是否失效呢嗅探操作:嗅探的缺点:再了解下JMM内存模型?Volatile可见性Volatile禁止指令重排序指令重排内存屏障volatile与synchronized的...

空空如也

空空如也

1 2 3 4 5 ... 13
收藏数 248
精华内容 99
关键字:

mesivolatile