文章目录

  • Java内存模型——JMM
  • 什么是JMM?
  • JMM三大特性
  • 硬件层基础知识
  • 存储器的层次结构
  • 总线锁
  • 数据一致性协议
  • 缓存行(cache line)
  • 缓存结构
  • 伪共享问题
  • 缓存行填充
  • 乱序问题
  • 产生原因
  • 合并写(write combining)
  • 如何通过硬件层控制不乱序


《深入理解Java虚拟机》和《Java并发编程的艺术》

Java内存模型——JMM

什么是JMM?

Java Memory Model(Java内存模型)

        JMM(Java内存模型)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM三大特性

  • 可见性
  • 原子性
  • 有序性

直接将JMM概念是又细又繁琐,我自己在学习过程中也费了很大的时间精力,这里就讲一些我简单的见解。想了解内存模型还得先知道计算机硬件的知识。

硬件层基础知识

存储器的层次结构

        首先我们应该了解CPU的存储层次结构,它是怎样进行缓存读取等操作的。硬件知识我只了解皮毛,这里放上硬件大佬的文章《CPU存储器层次结构》。

        目前大部分存储器是如下图这种金字塔结构的,读写是通过缓存进行。在这个金字塔中,由塔尖到底部缓存速度是由快到慢的,每相邻两层的读写速度差距非常大。

java js 内存模型 区别 java内存模型jmm_编程语言


        计算机的计算过程则是自下而上的,由各种输入设备输入信号暂存在内存中,内存再向上提交给CPU进行处理。

        而现在单核的设备已经很少见了,观察以上模型就会发现一个问题:若是CPU有两个核,内存向上传输数据时可能会加载到不同的CPU内部,这就会导致数据不一致的问题。

总线锁

        在早些时候为了避免发生上述的这种情况,计算机硬件在内存模块和CPU处理器之间加了一个“锁”,内存使用多线程来传输数据时将其中一个内核“锁”住,让数据只能传进另一个核,这样就保证了数据的一致性。

        但这种方法也带来了新的问题:效率低下

数据一致性协议

        在早些时候的计算机发现使用总线锁会使效率降低,所以就产生了一系列硬件底层的数据一致性协议:

  • MSI
  • MESI
  • MOSI
  • Synapse 、FireFly 、Dragon

        我们大部分人使用的是InterCPU,而Inter使用的是MESI协议,所以一般说计算机底层的数据一致性协议指的就是MESI

有关MESI的详细内容,《【并发编程】MESI–CPU缓存一致性协议》这篇文章讲的比较详细,大家感兴趣可以参考学习。
        MESI分别表示CPU中缓存数据的状态,简单理解就是:

  • M:我修改过;
  • E:我独享的;
  • S:与别人共享的;
  • I :别人改过的(我修改、读取无效)

        这种协议被称为缓存锁。而有些无法被缓存的数据还是得使用总线锁。所以总线锁 + 缓存锁 = 硬件层的数据一致性

缓存行(cache line)

缓存结构

        缓存系统中是以缓存行(cache line) 为单位存储的,缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节

伪共享问题

        位于同一缓存行的两个(或多个)不同数据,被不同的CPU锁定后读写产生互相影响的伪共享问题。

java js 内存模型 区别 java内存模型jmm_编程语言_02


        借用大佬的图来演示。如图,X、Y两个数据位于同一个缓存行,若Core1只改写X时会把X和Y一次都读进来,然后通知其他Core这个缓存行已经是“I”状态对其读写无效;同时若Core2只对Y进行改写,也会把包括X和Y在内的整个缓存行加载进来,也会使别的Core对次缓存行读写无效。这就是伪共享问题

缓存行填充

        因为CPU存在伪共享问题会使读写速率大打折扣。那怎么能解决这个问题提高速度呢?

        我们又知道CPU读取是以Cache Line为单位,而一行Cache Line通常又是64个字节,那么我们可以产生一个思路:若是通过定义别的内容把缓存行填充补齐,把X和Y分布在不同的缓存行中,CPU读取的时候是不是就不会发生伪共享问题?

        英国外汇交易公司LMAX开发的一个高性能队列Disruptor,他是这样做的:

java js 内存模型 区别 java内存模型jmm_jvm_03


        在CPU需要使用的数据前和后都放了7个Long类型无用数据,使其实际不论处于缓存中的什么位置,都能保证有用数据在单个缓存行内。

虽说大家都看得出来这么做会浪费一定的缓存空间,但速度提升带来的收益率远大于缓存损失的那些空间。

乱序问题

产生原因

        在讲存储器的硬件结构时我们知道CPU的读写速度非常快,要比内存快近百倍。而在CPU收到一条指令读写内存时,若是等待内存的结果返回会极大的浪费CPU性能。所以CPU在等待的同时会继续执行后续与内存返回结果无关的指令

        生动一点来讲这就类似我们生活中节省时间的效率问题,比如我在烧水的同时可以去洗茶杯放茶叶,但只有等水烧好了才能泡茶后喝茶。

合并写(write combining)

        CPU中L1和L2之间其实还有一个模块叫合并写缓存(WC Buffer)

        在CPU执行指令后计算结果返回给L1时,发现L1中并没有用于接收的值,就是缓存没有命中,然后就会把结果返回给L2。而L2比L1速度慢很多,CPU为了提高效率会把后续计算结果与其合并成一个结果,一起返回给L2。通常CPU合并写同一时刻只能拿到4个位置,并会以这4个位置为一个时间单位统一处理给L2。

如何通过硬件层控制不乱序

CPU内存屏障

有序性屏障

  • sfence
  • Ifence
  • mfence