volatile实现

作者: 忧从中来 | 来源:发表于2019-06-30 19:39 被阅读0次

volatile关键字有两方面的作用,一是保证共享变量可见性,二是禁止指令重排。

一、内存可见性

站在一个java程序员的角度,内存可见性应该从两个方面去理解,多核CPU的缓存一致性,以及JMM多线程线程栈本地内存与主存一致性。

1.1、CPU缓存一致性

现代CPU多采用多级缓存架构,


cpu缓存架构

缓存大大缩小了高速CPU与低速内存之间的差距。以三层缓存架构为例:
L1 Cache最接近CPU, 容量最小(如32K、64K等)、速度最高,每个核上都有一个L1 Cache。
L2 Cache容量更大(如256K)、速度更低, 一般情况下,每个核上都有一个独立的L2 Cache。
L3 Cache最接近内存,容量最大(如12MB),速度最低,在同一个CPU插槽之间的核共享一个L3 Cache。
在这种架构下,可能会存在下面的问题:
1、Core0与Core1命中了内存中的同一个地址,那么各自的L1 Cache会缓存同一份数据的副本。
2、最开始,Core0与Core1都在友善的读取这份数据。
3、突然,Core0要使坏了,它修改了这份数据,因为缓存的存在,这个修改并不会马上同步到主存,二十仅仅修改了Core 0 自己的L1 Cache中的值,此时Core1如果还继续以自己L1 Cache中的数据为准,必然导致错误的结果。

如何解决这个问题呢?缓存一致性协议MESI。
关于缓存一致性协议,可参考下面这篇博客,写得非常好。
https://www.cnblogs.com/yanlong300/p/8986041.html
简单来讲,就是当某一核改变了共享变量的值,cpu会发出一个指令,让其他核心L1 Cache中保存的值变成失效状态,当其他核心需要读取这个值时,需要到主存中重新加载。

1.2、工作内存与主存的一致性

Java工作内存与主存.png

java每个线程的工作内存都会有一个共享变量的备份,若一个线程中的值改变了而未同步到主存,另一个线程可能会读到脏数据。
若共享变量被定义未volatile,则:
写一个volatile变量时,JMM会把线程对应的工作内存中的变量值刷新到主存。
读一个volatile变量时,JMM会把线程对应的本地内存置为无效,然后从主存中读取该变量。

二、指令重排

指令重排分为编译器重排与处理器重排,JMM制定了如下的volatile重排序规则:

是否允许重排序 第二个操作 第二个操作 第二个操作
第一个操作 普通读/写 volatile读 volatile写
普通读写
volatile读
volatile写

可以看出:
1、当第二个操作是volatile写时,不论第一个操作是什么,都不允许重排。
2、当第一个操作是volatile读时,不论第二个操作是什么,都不允许重排。
3、当第一个操作是volatile写,第二个操作是volatile读时,不允许重排。

Java编译器通过插入内存屏障来实现以上规则。JMM内存屏障分为四种:

屏障类型 指令示例 说明
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

采用以下策略进行内存屏障插入:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的前面插入一个Load Load屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

参考
https://www.jianshu.com/p/64240319ed60
https://www.cnblogs.com/yanlong300/p/8986041.html
《Java并发编程的艺术》

相关文章

网友评论

    本文标题:volatile实现

    本文链接:https://www.haomeiwen.com/subject/lmdbcctx.html