Java内存模型

Posted by ooftf on May 23, 2021

Java内存模型(Java Memory Model,JMM)

JMM的由来

由于计算机的储存设备与处理器的运算速度有着几个数量级的差距,所以现代计算机系统都不得不加入一层或多层读写速度尽可能接近处理器运算速度的的高速缓存(Cache)来作为内存与处理器之间的的缓冲:将运算所需要的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无需等待缓慢的内存读写

CPU

Java中的“内存模型”可以理解为对硬件层“基于高速缓存的储存交互”的一种抽象,高速缓存对应工作内存,共享主内存对应主内存

CPU

主内存与工作内存间的交互操作

“交互操作”具体是指,一个变量如何从主内存拷贝到工作内存、如何从从工作内存同步回主内存的实现细节。Java内存定义了一下8种操作来完成

  1. lock(锁定)
  2. unlock(解锁)
  3. read(读取)
  4. load(载入)
  5. use(使用)
  6. assign(赋值)
  7. store(储存)
  8. write(写入)

缓存一致性问题

基于高速缓存的储存交互很好地解决了处理器与内存速度之间的矛盾,但也引入了一个新的问题:缓存一致性,每个处理器都有自己的高速缓存,而他们又共享同一主内存,当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。

指令重排序问题

为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化

可见性

除了Volatile之外,Java还有两个关键字能实现可见性

  • synchronized

    同步代码块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)” 这条规则获得的。

  • final

    final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程中就能看见final字段的值。

JVM规范的四种内存屏障

JVM的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。

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

volatile 是如何使用内存屏障实现的

  • 在每个volatile write 前插入 StoreStore 屏障,在 write 后插入StoreLoad屏障;
  • 在每个volatile read 前插入 LoadLoad 屏障,在 read 后插入LoadStore屏障;

Volatile 在汇编语言层次 是如何实现了JVM 对Volatile变量定义的规则呢?

Volatile变量赋值后会多执行一个lock空操作(这个lock不是主内存与工作内存交互的lock而是汇编指令lock),这个操作的作用相当于一个内存屏障(Merrory Barrier 或 Merrory Fence,指令重排序时不能把后面的指令重排序到内存屏障之前的位置)。 Lock指令的作用时将本处理器的缓存写入了主内存,该写入动作也会引起别的处理器或者内核缓存无效化。所以通过这个lock空操作,可以让volatile变量的修改对其他处理器立刻可见。

汇编指令 lock 在硬件层是如何实现的呢

通过 MESI 缓存一致性协议

MESI中每个缓存行都有四个状态,分别是E(exclusive)、M(modified)、S(shared)、I(invalid)。下面我们介绍一下这四个状态分别代表什么意思。

  • M:代表该缓存行中的内容被修改了,并且该缓存行只被缓存在该CPU中。这个状态的缓存行中的数据和内存中的不一样,在未来的某个时刻它会被写入到内存中(当其他CPU要读取该缓存行的内容时。或者其他CPU要修改该缓存对应的内存中的内容时(个人理解CPU要修改该内存时先要读取到缓存中再进行修改),这样的话和读取缓存中的内容其实是一个道理)。

  • E:E代表该缓存行对应内存中的内容只被该CPU缓存,其他CPU没有缓存该缓存对应内存行中的内容。这个状态的缓存行中的内容和内存中的内容一致。该缓存可以在任何其他CPU读取该缓存对应内存中的内容时变成S状态。或者本地处理器写该缓存就会变成M状态。

  • S:该状态意味着数据不止存在本地CPU缓存中,还存在别的CPU的缓存中。这个状态的数据和内存中的数据是一致的。当有一个CPU修改该缓存行对应的内存的内容时会使该缓存行变成 I 状态。

  • I:代表该缓存行中的内容时无效的。