JVM - JMM 内存模型及volatile

334 阅读5分钟

硬件层

缓存锁:

 MESI协议
 缓存中数据4种状态

 Modified   CPU自己修改过
 Exclusive  独占
 Shared     共享的
 Invaild    其他CPU修改过

但是有些无法被缓存的数据或者跨域多个缓存行的数据 依旧需要使用总线锁

缓存行

cpu读取缓存的时候以缓存行CacheLine为基本单位,目前大部分实现为64个字节 MESI 标记的数据是整个缓存行

 比如当2int互相不相关 位于同一缓存行 被两个不同cpu锁定 产生互相影响产生伪共享问题
 

解决办法 可以参照disruptor中环形指针 前后补数据 来确保被执行的数据在单独缓存行中

 public long p1, p2, p3, p4, p5, p6, p7; // cache line padding
  private volatile long cursor = INITIAL_CURSOR_VALUE;
  public long p8, p9, p10, p11, p12, p13, p14; // cache line padding

处理器乱序问题

CPU缓存结构

image.png

L3是cpu共享的缓存区

L2和L1是每一个CPU自己内部的缓存区

CPU为了提高效率 在确保指令无依赖关系的情况下,会在一条指令执行过程中 执行下面指令 比如

合并写操作 写1 写2 写3 然后同时往l1缓存区再写
读操作 读1 读2 是不冲突的就可能会是先读2 再读1

如何保证有序

内存屏障

X86 CPU原语内存屏障指令 屏障两侧的指令不可以重排

 sfence: 在sfence指令前 写的操作 必须在sfence指令后的写操作之前完成 
 
 lfence: 在lfence指令前的读操作 必须在lfence之后的读操作之前完成
 
 mfence: 在mfence指令前的读写操作 必须在mfence指令后的读写操作之前完成,保证系统在后面的memory访问之前,先前的memory访问都已经结束

Lock 指令

在执行指令的时候 读取到的内存 不能被其他cpu改变

Java中JMM模型实现

三大原则

可见性

     CPU1 写入的数据 对CPU2是可见的
     也就是CPU2能知道 对应的数据被CPU1修改过 需要去主内存去从新读取对应新的数据

原子性

   在CPU1修改某个数据的时候 其他CPU不准许修改对应的值 比如锁的机制

顺序性

  CPU在执行一段语句的时候 可能会优化对应指令数据 比如非依赖关系的指令顺序可能会被重排序执行
  通过缓存锁机制或者内存屏障的方式保证执行顺序不会被打乱

主存储区(内存)

主内存区 指的是多个CPU都可以访问的一个公共内存区,CPU读取数据的时候会有从主存储区读取到独占内存去也就是工作内存区中,读取的是主存储的一个副本数据

工作存储区(内存)

CPU在执行指令的时候 会将主存储区的数据读取到自己对应的工作存储区,然后进行读写运算等操作

内存屏障

理解内存屏障的方式有很多,我一般理解为,给CPU一个信号,告诉CPU两侧数据不能被打乱顺序执行哪怕是非依赖关系的指令

LoadLoad
   双读屏障
   
  load1 //读操作
  LoadLoad //读屏障
  load2 //读操作
  
  确保load1在加载数据之前 load2以及后续的load 不准许加载数据 
  必须要等load1加载玩数据之后再执行load2以及后续的加载数据
   
StoreStore
  双写屏障
   
  write1 //写操作
  StoreStore //写屏障
  write2 //写操作
  
  确保 store1 在写入之前 不会执行 store2以及后续的写入

StoreLoad
  写读屏障
 
  write1 //写操作
  StoreLoad //写读屏障
  load1 //读操作
  
  确保write1在写完数据后 load1才能加载数据

LoadStore
   读写屏障
   
   load1 //读操作
   LoadStore //读写屏障
   write1 //写操作
   
   确保 load1在加载完数据之后 write1才能写数据

volatile

字节码编译 就是访问标记 也就是 0x0040 [volatile]

也就volatile 是JVM实现的

volatile 保证了读写的顺序性 和 可见性 但是不保证原子性

保证读写的顺序 采用的是内存屏障的方式 volatile 读操作

 .... //之前的操作
 LoadLoad 在之前的读完之后自己才能读取 在读操作之前让之前的读操作读完了之后自己再去读
 volatile 读操作
 StoreLoad 禁止下面的写操作和当前的读操作乱序,就是确保当前的读操作读完之后 后面的写操作才能执行
 ......//之后的操作

volatile 写操作

  .... //之前的操作
  StoreStore 在之前的写操作完成之后自己才能去写
  volatile 写操作
  LoadStore 在自己的写完成之后 其他的读操作才能去读
  ......//之后的操作

image.png

JMM (2).jpg

保证可见性

MESI协议是可以实现保证可见性 
Lock指令 也就是在写操作前面添加Lock指令保证可见性 写完之后会将数据立刻刷写到主存储区中 Lock指令是CPU的指令 

happens-before

happens-before表示的是前一个操作的结果对于后续操作是可见的,它是一种表达多个线程之间对于内存的可见性。所以我们可以认为在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在happens-before关系。这两个操作可以是同一个线程,也可以是不同的线程