磕叨

因为大学没学过java,都是学C++的,工作时阴差阳错地走歪了,现在成了一个写业务代码的程序猿,平时工作多写java,一部分golang和js,能让我走歪了也不太走不太差的原因大概是因为大学所学的编译原理和后来自己在图书馆所学的编程语言范式(函数范式和OO),还有DDD吧。

但其实我多语言的特性了解得非常模糊的,而且这个东西随着语言工具的新版演进有所舍弃和新增,所以最近决定复习下编译原理,然后系统的学习下jvm(源码),也算一个进阶的必经之路吧,为什么选jvm,因为java写业务代码,还是思路挺畅顺的,另外据说jvm是现在能见到的最好的vm(如果是最好,那java为什么没有async/await呢)。

好了,本篇要说的是内存一致性模型,因为要了解一下基本的概念才好继续深入vm底层,在看JMM时提到一个有意思的东西---内存屏障,以前只知道他的作用,保证多线程执行环境下变量的状态符合预期结果。内存一致性模型不是有java才关注的,现在的多核计算机编程基本都应该会面临内存一致性问题。

(网文很多,这个权当学习的笔记,加深下印象)


起因

在芯片设计的领域,在单芯主频提高慢慢地越来越难,然后某一刻走向了多核时代(😹这段可以自己Google去)。这样,CPU里有多于个1个核心,在同一时间CPU能够同时运行多个线程,那么系统的处理能力就得到大大的提升。也带来了一些副作用,那就是内存一致性的问题。

对CPU而言,主存实在太慢了,所以芯片设计者为CPU设计了高速缓存(高速缓存技术早就出现了)。而起初只有一个高速缓存,后来CPU越跑越快,核心数越来越多,芯片设计者为CPU分级缓存,😹后来更复杂了多级缓存了,带来的性能提升也是丧心病狂的,哈哈,但是基本模型就是这样 独立的cache(L1) -> 核心共享的cache(L2) -> 主存(RAM)。我们就在这个基本的模型上展开吧。

cpu-内存存储体系基本模型

但是同时也引入了一些问题。因为而处理核心承载的线程并不是老死不相来往的,现实中很可能他们在处理同一份相关的数据基本的模型。


演进

内存一致性模型和面临情况

  • 下面就从最严格的的模型开始一步一步放开约束,变得越来越宽松。

a.) 顺序存储模型 SC(sequential consistency model)

  • 顺序存储模型是最基本的存储模型,也是最符合人脑思维的模型。CPU会按照程序中顺序依次执行store和load指令,(为了方便理解,这里假设cache是完美一致的,没有缓存间的同步问题)

    分析一下代码

    core1 core2
    S1: store data=NEW  
    S2: store flag=SET L1: load r1=flag
      B1: if (r1≠SET) goto L1
      L2: load r2=data

    在顺序存储器模型里,会严格按照代码指令流来执行代码,上面代码在主存里的访问顺序是:

      S1 S2 L1 L2

    其访问行为与UP(单核)上是一致的。我们能得到期望的数据状态,即r2的值为NEW。

b.) 完全存储定序 TSO(total store order)

  • (这里开始烧脑了哦)

    按照上面那个两级告诉缓存和主存的模型,假设最初的变量data仅仅存在主存里,那么core1执行store data=NEW这个cpu指令时,就要先将他从主存加载到缓存(以缓存行的形式),而这个过程很可能过了几个时钟周期了,所以芯片设计人员为这个耗时的过程设计了一个store buffer,它的作用是为store指令提供缓冲,使得CPU不用等待存储器的响应。只要store buffer里还有空间,写就只需要1个时钟周期。但这里也引入了另一个问题---访问乱序。

    引入store buffer后的模型

    相比于以前的内存模型而言,store的时候数据会先被放到store buffer里面,然后再被写到L1 cache里。

    core1 -
    S1: store flag=SET  
    S2: load r1=data  
    S3: store b=SET  

    如果按照顺序存储模型,S1肯定会比S2先执行。S1将指令放到了store buffer后会立刻返回,这个时候会立刻执行S2。S2是read指令,CPU必须等到数据读取到r1后才会继续执行。这样很可能S1的store flag=set指令还在store buffer上,而S2的load指令可能已经执行完(特别是data在cache上存在,而flag没在cache中的时候。这个时候CPU往往会先执行S2,这样可以减少等待时间)。这里就可以看出再加入了store buffer之后,内存一致性模型就发生了改变。

    如果我们定义store buffer必须严格按照FIFO的次序将数据发送到主存(所谓的FIFO表示先进入store buffer的指令数据必须先于后面的指令数据写到存储器中),这样S3必须要在S1之后执行,CPU能够保证store指令的存储顺序,这种内存模型就叫做完全存储定序(TSO)。

    core1 core2
    S1: store data=NEW L1: store flag=NEW
    S2: load r1=flag L2: load r1=data
       
    • 按照SC模型,可能发生如下顺序

        S1 S2 L1 L2   S1 L1 S2 L2   S1 L1 L2 S2   L1 L2 S1 S2   L1 S1 S2 L2   L1 S1 L2 S2

      最终我们看到的结果是至少有一个CORE的r1值为