java volatile关键字作用及实现原理

一、volatile解决了什么问题

任何事物的存在必有其理由,那么java语言的设计者设计volatile的理由是什么呢——当然是为了解决某些问题。

二、java内存模型——伴生的两个问题

这些问题来源于java的内存模型,如下图:
java volatile关键字作用及实现原理_第1张图片

什么是java内存模型?

java内存模型(JMM)定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory,也叫工作内存),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

简单的说,我们都知道在内存和cpu之间还有L1、L2、L3缓存等空间,它们的速度是远远超过内存的。所以程序语言的设计者自然会想办法利用这些空间,以提升程序速度。 一个可行的方案就是,当线程需要处理一些object的时候,把它们复制一份,存到这些空间里面,然后在这些空间进行操作,等操作完了再回写到主内存里面。这就是所谓的“工作内存”。

但是,由此也引发了一些问题。

1.可见性问题

CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。但这个变更对运行在右边CPU中的线程在一段时间内是不可见的,因为这个更改可能还没有flush到主存中。
java volatile关键字作用及实现原理_第2张图片

2.重排序问题

可见性问题相对比较容易理解,重排序问题则更为复杂一些。
在《java语言规范》书中,描述了这样一种场景:
在这里插入图片描述
A、B、r1、r2是四个共享变量,A和B初值都=0。 现在有两个线程,分别执行代码:

1.	r2 = A;
2.	B = 1;
3.	r1 = B;
4.	A = 2;

猜猜r1和r2最终可能被赋了哪些值?
直觉上,由于线程执行顺序问题,存在这些可能的执行顺序:
1234、 1324、 1342、 3124、3142、3412
结果分别是:
(r2=0, r1=1),(r2=0, r1=0), (r2=0,r2=0), (r1=0,r2=0), (r1=0, r2=0), (r1=0, r2=2)

去重后,理论上应只存在(r2=0, r1=1),(r2=0, r1=0), (r1=0, r2=2)。

然而事实上,却有可能出现r2=2, r1=1这种看似不可能的可能性。

导致这种情况发生的原因就是指令重排: 上例中的指令可能会被排列成2412这种顺序去执行了。

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。所以,你写的代码最终是这样被执行的:
在这里插入图片描述
从上图可以看出,存在三种指令重排序的来源:

1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

volatile关键字的作用

简单来说,volatile关键字的作用就是解决了上述的两个问题,即:
1. 保证可见性 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

2. 禁止重排序 volatile重排序规则如下:
java volatile关键字作用及实现原理_第3张图片

volatile关键字的原理

那么,volatile是怎么达到这样的效果的呢?
这又引出了一个概念:内存屏障

关于内存屏障的概念可以参加我的这篇转载博文:java内存屏障

说白了,就是在处理volatile修饰的变量时,自动加上基于一组cpu指令,可以让缓存强制同步到内存,或者让缓存直接失效的jvm指令组合。参见下表:

java volatile关键字作用及实现原理_第4张图片

参考资料:深入理解JVM-内存模型(jmm)和GC

你可能感兴趣的:(jvm,多线程)