java编程,如何彻底理解volatile关键字?( 二 )


M(modify):
I(invalid)
E(Exclusive)
S(Share)

java编程,如何彻底理解volatile关键字?

文章插图
JMM内存模型的八种同步操作
java编程,如何彻底理解volatile关键字?

文章插图
1、read(读取) 。从主内存读取数据
2、load(载入):将主内存读取到的数据写入到工作内存
3、use(使用): 从工作内存读取数据来计算
4、assign(赋值):将计算好的值重新赋值到工作内存中
5、store(存储):将工作内存数据写入主内存
6、write(写入):将store过去的变量值赋值给主内存中的变量
7、lock(锁定):将主内存变量加锁 。标识为线程 独占状态
8、unlock(解锁):将主内存变量解锁 。解锁后其他线程可以锁定该变量
Java 内存模型带来的问题
1、可见性问题
java编程,如何彻底理解volatile关键字?

文章插图
左边CPU中运行的线程从主内存中拷贝对象obj到它的CPU缓存 。把对象obj的count变量改为2 。但这个变更对运行在右边的CPU中的线程是不可见 。因为这个更改还没有flush到主内存中 。
在多线程环境下 。如果某个线程首次读取共享变量 。则首先到主内存中获取该变量 。然后存入到工作内存中 。以后只需要在工作内存中读取该变量即可 。同样如果对该变量执行了修改的操作 。则先将新值写入工作内存中 。然后再刷新至于内存中 。但是什么时候最新的值会被刷新到主内存中是不太确定的 。一般来说是很快的 。但是具体时间未知 。。要解决共享对象可见性问题 。我们可以使用volatile关键字或者加锁 。
2、竞争问题
java编程,如何彻底理解volatile关键字?

文章插图
线程A 和 线程B 共享一个对象obj, 假设线程A从主存读取obj.count变量到自己的缓存中 。同时 。线程B也读取了obj.count变量到它的CPU缓存 。并且这两个线程都对obj.count做了加1操作 。此时 。obj.count加1操作被执行了两次 。不过都在不同的CPU缓存中 。
如果则两个加1操作是串行执行的 。那么obj.count变量便会在原始值上加2 。最终主内存中obj.count的值会为3 。然后图中两个加1操作是并行的 。不管是线程A还是线程B先flush计算结果到主存 。最终主存中的obj.count只会增加1次变成2 。尽管一共有两次加1操作 。要解决上面的问题我们可以使用synchronized 代码块 。
3、重排序
除了共享内存和工作内存带来的问题 。还存在重排序的问题 。在执行程序时 。为了提高性能 。编译器和处理器常常会对指令做重排序 。
重排序分3中类型:
(1) 编译器优化的重排序 。
(2) 指令级并行的重排序
(3)内存系统的重排序
① 数据依赖性
数据依赖性: 如果两个操作访问同一变量 。且这两个操作中有一个为写 。此时这两个操作之间就存在数据依赖性 。
依赖性分为以下三种:
java编程,如何彻底理解volatile关键字?

文章插图
java编程,如何彻底理解volatile关键字?

文章插图
上图很明显 。A和C存在数据依赖 。B和C也存在数据依赖 。而A和B之间不存在数据依赖 。如果重排序了A和C或者B和C的执行顺序 。程序的执行结果就会被改变 。
很明显 。不管如何重排序 。都必须保证代码在单线程下的运行正确 。连单线程下都无法保证 。更不用讨论多线程并发的情况 。所以就提出一个as - if -serial 的概念 。
4、as - if -serial
意思是:不管怎么重排序(编译器和处理器为了提高并行度) 。(单线程)程序的执行结果不能被改变 。编译器、runtime和处理器都必须遵守as - if -serial 语义 。
java编程,如何彻底理解volatile关键字?

文章插图
java编程,如何彻底理解volatile关键字?

文章插图
A和C之间存在数据依赖 。同时B和C之间也存在数据依赖关系 。因此在最终执行的指令序列中 。C不能被重排序A和B的前面(C排到A和B的前面 。程序的结果将会被改变) 。但A和B之间没有数据依赖关系 。编译器和处理器可以重排序A和B之间的执行顺序 。
as - if -serial 语义把单线程程序保护了起来 。遵守as-if-serial语义的编译器、runtime和处理器可以让我们感觉到: 单线程程序看起来是按程序的顺序来执行的 。as-if-srial语义使单线程程序无需担心重排序干扰他们 。也无需担心内存可见性的问题 。
5、内存屏障

推荐阅读