java之volatile内存屏障

JVM内存屏障

内存屏障作用

处理器提供内存屏障指令(Memory Barrier):

  • 写内存屏障(Store Memory Barrier):处理器将存储缓存值写回主存(阻塞方式)。
  • 读内存屏障(Load Memory Barrier):处理器,处理失效队列(阻塞方式)。

内存屏障种类

屏障类型 指令示例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作
StoreStore Barriers Store1;StoreStore;Store2 该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作
LoadStore Barriers Load1;LoadStore;Store2 确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作
StoreLoad Barriers Store1;StoreLoad;Load2 该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令

StoreLoad Barriers同时具备其他三个屏障的效果,因此也称之为全能屏障(mfence),是目前大多数处理器所支持的;但是相对其他屏障,该屏障的开销相对昂贵。

cpu指令

Store Barrier

即写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见。

sfence指令实现了Store Barrier,相当于StoreStore Barriers。

强制所有在sfence指令之前的store指令,都在该sfence指令执行之前被执行,发送缓存失效信号,并把store buffer中的数据刷出到CPU的L1 Cache中;所有在sfence指令之后的store指令,都在该sfence指令执行之后被执行。即,禁止对sfence指令前后store指令的重排序跨越sfence指令,使所有Store Barrier之前发生的内存更新都是可见的。

Load Barrier

即读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据

lfence指令实现了Load Barrier,相当于LoadLoad Barriers。

强制所有在lfence指令之后的load指令,都在该lfence指令执行之后被执行,并且一直等到load buffer被该CPU读完才能执行之后的load指令(发现缓存失效后发起的刷入)。即,禁止对lfence指令前后load指令的重排序跨越lfence指令,配合Store Barrier,使所有Store Barrier之前发生的内存更新,对Load Barrier之后的load操作都是可见的。

Full Barrier

mfence指令实现了Full Barrier,相当于StoreLoad Barriers。

mfence指令综合了sfence指令与lfence指令的作用,强制所有在mfence指令之前的store/load指令,都在该mfence指令执行之前被执行;所有在mfence指令之后的store/load指令,都在该mfence指令执行之后被执行。即,禁止对mfence指令前后store/load指令的重排序跨越mfence指令,使所有Full Barrier之前发生的操作,对所有Full Barrier之后的操作都是可见的。

volatile

x86中的实现

  • 在写volatile变量v之后,插入一个sfence。这样,sfence之前的所有store(包括写v)不会被重排序到sfence之后,sfence之后的所有store不会被重排序到sfence之前,禁用跨sfence的store重排序;且sfence之前修改的值都会被写回缓存,并标记其他CPU中的缓存失效。
  • 在读volatile变量v之前,插入一个lfence。这样,lfence之后的load(包括读v)不会被重排序到lfence之前,lfence之前的load不会被重排序到lfence之后,禁用跨lfence的load重排序;且lfence之后,会首先刷新无效缓存,从而得到最新的修改值,与sfence配合保证内存可见性。

在另外一些平台上,JVM使用mfence代替sfence与lfence,实现更强的语义。

其它实现

  • 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
  • 在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;

volatile 性能:

volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

内存屏障在java中的其它应用

  • lock:解锁时,jvm会强制刷新cpu缓存,导致当前线程更改,对其他线程可见。

  • final:即时编译器在final写操作后,会插入内存屏障,来禁止重排序,保证可见性

总结

volatile有两个作用,分别为禁止指令重排及内存可见性

volatile通过向jvm指令中添加StoreLoad等指令,及cpu中添加mfence达到该效果

内存可见性指

  • cpu写入后立即刷新内存
  • 读取时jvm jit不缓存内存中读取的变量(非volatile变量jvm从内存读取时会缓存至局部变量,以便再次使用)
  • cpu在读取字段前将缓存字段失效,强制读取内存

禁止指令重排指jit中保证volatile的执行顺序,cpu执行时同样保证指令执行顺序