Volatile最终解释

1.JMM模型

JMM模型提出了java的多线程工作模型,它不去为每种CPU(Intel,AMD),多核心或者单核心,以及多缓存无缓存还是几级缓存,做具体的描述,而是给出了一种Java层面的工作模型,以线程,线程独立空间为描述,给出一种让不懂底层的开发者可以理解的并发问题模型 ​ 有了这种模型,进而引出原子性,可见性,有序性这些并发概念,但是想要真正解决问题是要JVM针对不同的硬件做出处理才能达到效果,对于我们上层应用只需要使用synchronize和volatile就可以了。

2.MESI协议

对于Intel的X86CPU来说,MESI缓存行协议,它和上层应用没有任何关系,只要使用这种类型的CPU,开启MESI它就是存在的,对于2核CPU来说,两个L2或者L1缓存,只要数据被写入到缓存中,那么另一个缓存就立即被可见 MESI除了提供缓存中内容可见性,还提供了锁缓存行的支持 ​ 这里提出一个问题,既然缓存行中的数据没有问题,我们的Java程序为什么还会出现可见性问题?   也就是一个线程操作了数据,数据被写入到缓存中另一个线程有时可见,有时很久不可见呢?

有了MESI协议,尽管多核CPU逻辑上是这样的,但是L1,L2也相当于共享内存,只要在L1,L2中有相同数据就是立即可见的

3.StoreBuffer

1.可见性问题

1. 在CPU的架构设计中我们忽略了一个东西就是StoreBuffer,它是确确实实存在的,但是因为Intel不开源,我们可以通过测试发现如果数据放入到各自CPU核心运行线程的StoreBuffer中,数据是不被可见的
​
2.除了StoreBuffer没有被放入到缓存中不可见,写入L1,L2,L3高速缓存的时候,intel有个规定,MESI的硬性规定就是如果写一份数据,那个L1.L2.L3中必须有这个数据,如果没有要先从内存读取出来,但是如果两个核心cpu就有两个Store buffer,如果其中一个将新值放入到Store buffer中了,而L3缓存中没有,去内存中读老值放到L3中,此时cpu2直接将L3的老值拿走了,那么这个新值不可见
    这种情况叫StoreLoad问题,这种情况其实是两个线程的读写乱系问题,最终会以内存可见性问题体现
​
这也就说明只有MESI还不能保证立即可见,因为上面还有一层,这一层无法控制,所以以上两种情况都是不可见的原因

 

上述问题如何解决?
要解决这个还需要根据Intel的设计架构,Intel设计有很多代,但是不管哪一代都有StoreBuffer存在,但是有一些有缓存行L1,L2,L3等,有一些没有,那么就要通过不同的方案解决
1. 有缓存行,有MESI协议,通过指令的Lock前缀,能使用MESI的锁缓存行的支持让缓存行立即锁住,然后将StoreBuffer的数据立即刷入
2. 无缓存行,直接到共享内存,通过指令的Lock前缀,采用总线锁的方式
​
这也是为什么在早期CPU无多级缓存,慢的原因,总线锁属于硬件层面,MESI通过软件层面进行优化了

2.内存写入乱序问题

 

对于一个有缓存MESI协议的这种情况,MESI的硬性规定就是如果写一份数据,那个L1.L2.L3中必须有这个数据,如果a2在缓存中有数据,而a3没有,那么有一些CPU就会让a2先写,a3慢慢的让缓存去内存读,这就是内存写入乱序问题
​
幸好Intel的X86没有做这种优化,因此没有内存写入乱序问题,LoadStore,LoadLoad,StoreStore问题在这种处理器都是不存在的,因此只有上述的StoreLoad问题

4.Volatile

1.C语言的Volatile

 保证编译器不优化,从而每次读取完整执行过程,不在寄存器中缓存数据 与之同功能的还有 1.直接关闭c语言编译器优化,显然一般不会使用,代码一般需要优化 2.使用编译器屏障 _asm_volatile_(“;;;“   “memory“) 3.使用volatile ​ 粒度 关闭>volatile>编译器屏障 ​ 上述的这些操作即保证数据可见性,也保证了指令有序性

2.Java中的Volatile

StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
​
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
​
在JVM中,使用volatile会解析为上面的一些列指令,它们交给解释器来执行,但是解析器有不同的种类

1.使用C++解释器

#define READ_MEM_BARRIER __asm __volatile ("":::"memory")
#define WRITE_MEM_BARRIER __asm __volatile ("":::"memory")
​
inline void OrderAccess::loadload()   { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore()  { acquire(); }
inline void OrderAccess::storeload()  { fence(); }
​
inline void OrderAccess::acquire() {
  READ_MEM_BARRIER;
}
inline void OrderAccess::release() {
  WRITE_MEM_BARRIER;
}
inline void OrderAccess::fence() {
  FULL_MEM_BARRIER;
}
就使用了C语言中的编译器中的编译器屏障

2.使用模板解释器

inline void OrderAccess::loadload()   { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore()  { acquire(); }
inline void OrderAccess::storeload()  { fence(); }
​
inline void OrderAccess::acquire() {
  volatile intptr_t local_dummy;
#ifdef AMD64
  __asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
  __asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64
}
​
inline void OrderAccess::release() {
  // Avoid hitting the same cache-line from
  // different threads.
  volatile jint local_dummy = 0;
}
​
inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
    // 因为mfence很昂贵
#ifdef AMD64    //看lock前缀
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}
只有StoreLoad写操作才会将数据更改,所以只有它使用了Lock前缀强制将新值刷出去
​
就是使用了lock前缀,直接锁缓存行将StoreBuffer刷出去,就足够保证可见性了,同时lock前缀还有编译器屏障的作用
并没有使用mfence,这个会清空流水线,开销非常大

3.总结

Java中的volatile与软件硬件都相关,软件层面使用了 编译器屏障,禁止直接从寄存器获取数据,保证了可见性,每次都从内存取而不是取寄存器中的,硬件层面使用 Lock前缀+MESI缓存一致性特性快速写出StoreBuffer数据,也保证了可见性
​
有序性是通过 编译器屏障,禁止编译器优化指令顺序,指令执行乱序在CPU中也帮我们规避掉了,Lock前缀+MESI也解决了多线程读写有序性问题
​
在单例模式DCL必须加volatile中,主要使用的是 单线程中编译器屏障,禁止编译器优化指令顺序,使其单例对象按顺序完成初始化的整个流程
在很多博文中,可见性和有序性是分开来谈的,但是通过软件和硬件层面我们可以发现,可见性和有序性是共同出现的,并且使用一个技术可以将两个问题共同解决,编译器屏障既解决了可见性也解决了有序性,Lock刷新同样也是
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值