Java内存模型及orderAccess类介绍

从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

深入理解Java内存模型(一)——基础

上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

一个 happens-before 规则通常对应于多个编译器重排序规则和处理器重排序规则。 

处理器重排序与内存屏障指令

现代的处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读 / 写操作的执行顺序,不一定与内存实际发生的读 / 写操作顺序一致!为了具体说明,请看下面示例:

Processor A Processor B
a = 1; 

x = b; 
b = 2; 

y = a; 
初始状态:a = b = 0

处理器允许执行后得到结果:x = y = 0

假设处理器 A 和处理器 B 按程序的顺序并行执行内存访问,最终却可能得到 x = y = 0 的结果。

这里处理器 A 和处理器 B 可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的脏数据刷新到内存中(A3,B3)。当以这种时序执行时,程序就可以得到 x = y = 0 的结果。

从内存操作实际发生的顺序来看,直到处理器 A 执行 A3 来刷新自己的写缓存区,写操作 A1 才算真正执行了。虽然处理器 A 执行内存操作的顺序为:A1->A2,但内存操作实际发生的顺序却是:A2->A1。此时,处理器 A 的内存操作顺序被重排序了(处理器 B 的情况和处理器 A 一样,这里就不赘述了)。

这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写 - 读操做重排序。

扫描二维码关注公众号,回复: 7157139 查看本文章

下面是常见处理器允许的重排序类型的列表:

  Load-Load Load-Store Store-Store Store-Load 数据依赖
x86 N N N Y N

上表单元格中的“N”表示处理器不允许两个操作重排序,“Y”表示允许重排序。

上表中的 x86 包括 x64 及 AMD64。

为了保证内存可见性,java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障指令分为下列四类:

JMM(Java内存模型)JMM来源于JSR-133(http://gee.cs.oswego.edu/dl/jmm/cookbook.html)。

JMM的目的是为了解决Java多线程对共享数据的读写一致性问题,通过Happens-Before语义定义了Java程序对数据的访问规则,修正之前由于读写冲突导致的Cache数据不一致的问题。具体到Hotspot VM的实现,主要是由OrderAccess类定义的一些列的读写屏障来实现JMM的语义。

在所有的 X86 CPU 上都具有锁定一个特定内存地址的能力,当这个特定内存地址被锁定后,它就可以阻止其他的系统总线读取或修改这个内存地址。这种能力是通过 LOCK 指令前缀再加上下面的汇编指令来实现的。当使用 LOCK 指令前缀时,它会使 CPU 宣告一个 LOCK# 信号,这样就能确保在多处理器系统或多线程竞争的环境下互斥地使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。
能够和 LOCK 指令前缀一起使用的指令如下所示:

BT, BTS, BTR, BTC   位检测指令
XCHG, XADD  
ADD, OR, ADC, SBB  
AND, SUB, XOR   
NOT, NEG, INC, DEC  
注意:XCHG 和 XADD (以及所有以 'X' 开头的指令)都能够保证在多处理器系统下的原子操作,它们总会宣告一个 "LOCK#" 信号,而不管有没有 LOCK 前缀。

参考:

(1)JSR 133 (Java Memory Model) 中文版 http://ifeve.com/wp-content/uploads/2014/03/JSR133%E4%B8%AD%E6%96%87%E7%89%881.pdf

(2)The JSR-133 Cookbook for Compiler Writers 英文版 http://gee.cs.oswego.edu/dl/jmm/cookbook.html

(3)Synchronization and the Java Memory Model 英文版 http://gee.cs.oswego.edu/dl/cpj/jmm.html

(4)深入理解 Java 内存模型 https://www.infoq.cn/article/java-memory-model-1/

猜你喜欢

转载自www.cnblogs.com/mazhimazhi/p/11447586.html