JSR-133规范详解

JSR-133规范详解

JSR-133规范,JavaTM内存模型与线程规范.该规范语义不会去描述多线程程序该如何执行。
而是描述多线程程序允许表现出的行为。

JVM内存模型和Java内存模型

JMM来源于JSR-133,主要解决多线程对共享数据的读写一致性问题,
JVM内存模型则是指JVM的内存分区。两者本身没有关系。

共享变量/堆内存(Shared variables/Heap memory)

能够在线程间共享的内存称作共享内存或堆内存。所有的实例字段,静态字段以及数组元素都存储在堆内存
中。我们使用变量这个词来表示字段和数组元素。方法中的局部变量永远不会在线程间共享且不会被内存模型影响

线程间的动作(Inter-thread Actions)

线程间的动作是由某一线程执行,能被另一线程探测或直接影响的动作(action)。线程间的动作包括共享变量的读写以及同
步动作(synchronization action),如 lock 或 unlock 某个管程,读写某个 volatile 变量或启动一个线程。
也包括与外部世界交互的动作。

线程内语义(Intra-thread semantics)

线程内语义是单线程程序的标准语义,基于某个线程内读动作能看到的值,可以完整的预测这个线程的行为。线程内语义决定着某个线程孤立的执行过程;当从堆中读取值时,值是
由内存模型决定的。

同步动作(Synchronization Actions)

同步动作包括锁、解锁、读写 volatile 变量,用于启动线程的动作以及用于探测线程是否结束的动作。

volatile详细解释

jsr133强化了volatile语义,需要有acquire和release语义,对某个volatile字段的写操作happens-before每个后续对该volatile字段的读操作

jvm具体实现

jvm通过内存屏障来保障volatile语义,硬件层面的内存屏障分两种store Barrier和load Barrier,jvm通过两种组合成四种即loadload,
loadstore,storestore,storeload.

  • 对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据
  • 对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

cpu3级缓存介绍

CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快得多。
导致CPU可能会花费很长时间等待数据到来或把数据写入内存。

每一级缓存中所存储的数据全部都是下一级缓存中的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也相对递增。
当CPU要读取一个数据时,首先从一级缓存中查找,如果没有再从二级缓存中查找,如果还是没有再从三级缓存中或内存中查找。

lock前缀详细解释

演示java代码

在新增jvm参数之后可以打印对应的汇编指令

1
2
3
   
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:+TraceClassLoading

jvm并没有采用Barrier指令而是采用了lock addl $0x0,(%rsp)来实际上保障volatile语义。
lock是一个前缀指令,是和其他指令配合使用 addl $0x0,(%rsp)实际上就是+0,是一个无效指令,配合lock前缀锁定cpu总线(实际上是锁缓存),
lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据
相关资料
https://software.intel.com/sites/default/files/managed/a4/60/325384-sdm-vol-3abcd.pdf 2.85

MESI协议介绍

缓存是分段(cache line),每次加载的缓存是定长的,通常是64位。lock其实不是锁总线是是锁定缓存段.
cpu缓存一致性协议,把cpu缓存行的数据分成4个状态,用2个bit表示

  • Modified 这行数据有效,这行数据修改了,和内存中数据不一致,数据只存在本cache中
  • Exclusive 这行数据有效,数据和内存中一致,数据只存在本cache中
  • Shared 这行数据有效,数据和内存中一致,数据存在很多cache中
  • Invaid 这行数据无效

其中M和E解决独占问题(也就是锁的语义),lock前缀配合一个写操作,整个缓存行的状态会从s->e->m->i->s

缓存伪共享和java8优化

cpu每次load数据是基于缓存行加载的,如果两个变量公用一个缓存行(一般是相同低bit位地址),就会产生缓存伪共享的问题,只打算更新a变量的只,结果导致b变量也一起更新。
解决的思路是把a变量独占一行,例如独占64位。Java8中已经提供了官方的解决方案,
Java8中新增了一个注解@sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置-XX:-RestrictContended才会生效。