volatile关键字
并发编程中的三个概念:原子性,可见性,有序性
volatile含义
保证了变量的可见性。通过"内存屏障"实现部分“有序性”。
java5版本增强语义,修复缓存导致的可见性问题,通过volatile的happens-before规则
它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
volatile汇编指令lock
加入volatile关键字时,汇编代码会多出一个lock前缀指令。lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
它会强制将对缓存的修改操作立即写入内存;
如果是写操作,它会导致其他CPU中对应的缓存行无效。
Happens-Before规则
Java使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
强调一个非常重要的概念:
如果A happens-before B,JMM并不要求A一定要在B之前执行,仅仅要求前一个操作(执行的结果)对后一个操作可见。如下代码为例
在单线程中,得出如下结论:
A happens-before B
B happens-before C
A happens-before C
其中1,2根据程序顺序性规则,而3根据传递性规则。但实际执行上B却可能在A之前执行。
该代码中操作A的执行结果不需要对操作B可见;而且重排序先B后A的执行结果,与先A后B按happens-before顺序执行的结果一致。在这种情况下,JMM会认为这种重排序并不非法(not illegal),允许这种重排序。
volatile在JDK5如何增强语义
如下代码,线程A执行writer方法,线程B执行reader()。在Jdk1.5之前,x可能是42也可能是0。在1.5以上版本x=42。1.5之前存在cpu缓存导致的可见性问题。
根据程序顺序性原则推出,a happens before b , c happens before d。
Java在单线程不影响执行结果的情况下,jvm可能对指令重排序。但是volatile修饰了v变量,它的“内存屏障”特性保证了执行到该行,前面的操作已经完成。所以在程序执行顺序中,x的赋值一定先于v的的赋值,a是先于b执行的。
jdk1.5增强了volatile的语义,增加了happens-before原则,对一个volatile变量的写,happens-before于任意后续对这个volatile变量的读。所以b happens before c。结合传递性不难得出:a happens before d。
再强调一下happens-before的语义,前一个操作的结果对后一个操作可见。所以此时a的结果对于d是可见的,x = 42。
而在jdk1.5之前,则没有b happens before c的规则,自然无法推出a happens before d。a的赋值操作对d无法保证是可见的。存在x写入到cpu缓存,未刷新到内存中的情况,此时则d处读取的值为0。jdk1.5后通过a appens before d保证了x的可见性,即x = 42。
volatile的应用场景
1. 双重检查的单例模式
这里的问题重点在于new()操作,这是一个复合操作。
我们理解的new()操作:
1.分配一块内存 M; 2.在内存 M 上初始化 Singleton 对象; 3.然后 M 的地址赋值给 instance 变量。
然而发生了重排序后,实际的new()操作:
1.分配一块内存 M;2.将 M 的地址赋值给 instance 变量;3.最后在内存 M 上初始化 Singleton 对象。
这是一个先将地址赋值给变量,后在内存上初始化对象的过程。假设线程A执行到c处,给instance变量赋值完但还没在内存上初始化对象,此时发生cpu切换,线程B执行,执行到a处判断instance != null,直接返回了instance,而此时实际上instance内存上还未初始化对象,产生异常。
而通过volatile的内存屏障功能,可以使上述过程正确执行,达到一个禁止重排序的效果。
参考:
《Java并发编程的艺术》
《Java并发编程实战》
Last updated
Was this helpful?