【java多线程系列】java中的volatile的内存语义

在java的多线程编程中,synchronized和volatile都扮演着重要的 角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性,可见性指的是当一个线程修改一个共享变量时,另一个线程能够读到这个修改后的值。如果volatile修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。本文将从volatile的JMM内存语义的角度带领大家全面认识volatile修饰符。

一volatile的特性

可见性:对volatile变量的读总是能看到任意线程对这个volatile变量最后的写入,即当一个线程修改了volatile变量的值,新值对于其他线程而言是可以立即得知的。而对于普通共享变量是做不到这点的,在前面的中讲过,为了提高处理速度,处理器不直接和内存打交道,而是先将内存数据读取到缓存中然后再进行操作,但操作之后不知道何时写回内存。

因此普通变量不能够做到一个线程修改了其值,新值对于其他线程而言是可以立即得知的,那么volatile是如何做到的呢?

这是因为含volatile修饰符的java代码在转换为汇编代码的时候会在代码中插入一个包含Lock前缀的指令代码,这个指令会做以下两件事:

1.将当前处理器缓存行的数据写回到系统内存。

2.这个写回内存的操作会使得其他CPU中缓存了该内存地址的数据无效。

那么为何添加这两个条件之后就可以做到新值对于其他线程而言是可以立即得知的呢?还是接着上面的过程分析,上面说到对于普通变量读取到缓存之后,不知道何时写回内存,而如果用volatile修饰的话,在进行写操作的时候,JVM会向处理器发送一条Lock前缀的指令,将这个变量缓存中的内容写回到系统内存,但是可能其它处理器在它写回内存之前就已经更新自己缓存中的数据,那么这样的话,仍然和上面一样不能保证结果的正确性,因此添加了第二条,即为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,即每个处理器会检查自己缓存中的数据是否过期,如果过期,则会将缓存行内容设置为无效,当处理器对这个缓存行数据进行操作的时候就会从系统内存读取数据,而Lock指令的第二条就是使其缓存无效,因此即使其它处理器在处理器A将缓存中的内容重新写回系统内存之前就读取了系统内存中的值,仍然可以保证其它处理器在使用自己缓存中的数据之前从内存中再读取一次数据(因为Lock指令的第二条使得其缓存中的值无效),这样就相当于线程A修改了volatile变量的值,其新值对于其他线程而言是可以立即得知的。(虽然这个过程仍然与普通变量一样要通过系统内存来实现,但在效果上或者说内存语义上与“新值对于其他线程而言是可以立即得知的”效果一样)



原子性:对任意单个volatile变量的读/写具有原子性,但类似volatile++这种复合操作不具备原子性。

对于volatile的原子性可以理解为对volatile变量的单个的读/写可以看做是使用同一个锁对这些单个的读/写操作作了同步操作。因为它们之间的执行效果是相同的。

二volatile写-读建立的happens-before关系

在前面的volatile的特性的第一条可见性我们解释了为何“一个线程修改了volatile变量的值,新值对于其他线程而言是可以立即得知的”,但是上面是在处理器实现的原理上进行分析的,事实上通过JMM内存模型定义的规则也可以推出上述结论。这就是volatile写-读建立的happens-before关系

从内存语义的角度来看,volatile的写-读与锁的释放-获取具备相同的内存效果:即volatile写和锁的释放内存语义相同,volatile读与锁的获取内存语义相同。而我们知道对于同一块内存的锁必须先释放后其它线程才可以获取,即一个线程释放锁一定在其它线程获取锁之前。因此一个线程对volatile的写肯定happens-before其它线程对volatiel的读。

下面我们基于上述特性来看一下volatile写-读建立的happens-before关系,代码如下:

class VolatileExample {
int a = 0;
volatile boolean flag = false; public void writer() {
a = 1; //1
flag = true; //2
} public void reader() {
if (flag) { //3
int i = a; //4
……
}
}
}

假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens before规则,这个过程建立的happens before 关系可以分为两类:





根据程序次序规则,1 happens before 2; 3 happens before 4。

根据volatile规则,2 happens before 3。

根据happens before 的传递性规则,1 happens before 4。

上述happens before 关系的图形化表现形式如下:

【java多线程系列】java中的volatile的内存语义

根据图示可以很容易的推出1 happens-before 4,即线程A修改了缓存中的共享变量,在线程B读取同一个共享变量时,读到的将是线程A修改之后的值(即最终写回主存中的值),相当于该共享变量立即对线程B可见。

这个图比前面的那段文字叙述理解起来容易的多,但那段文字叙述才是JMM实现的原理解释,希望读者能认真体会。

二volatile写-读的内存语义

关于volatiel写-读的内存语义,前面也提到过,这里再详细讲解一下:

volatile写的内存语义:写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。

以上面示例程序VolatileExample为例,假设线程A首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。下图是线程A执行volatile写后,共享变量的状态示意图:

【java多线程系列】java中的volatile的内存语义

可以看到线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。即volatile写的内存语义保证了本地内存与主存一致性。

volatile读的内存语义:读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

【java多线程系列】java中的volatile的内存语义

从图上可以看到,在读flag变量后,本地内存B已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值也变成一致的了。即volatile读的内存语义保证了读线程中本地内存与主存的一致性。

正是因为上述写的一致性与读的一致性保证了最终读的线程读取到的一定是写线程刷新后的值,从而保证内存的可见性。

以上就是本博客的主要内容,重点理解volatile的特性以及volatile写-读建立的happens-before关系是如何保证内存一致性的。

如果读者觉得本博客写的不错,记得小手一抖,点个赞哦!另外欢迎大家关注我的博客账号哦,将会不定期的为大家分享技术干货,福利多多哦!

上一篇:AOSP中的HLS协议解析


下一篇:Kickstart 自动化安装配置