Volatile
什么是volatile
1.Java语言规范第3版中对volatile的定义如下:
Java编程语言允许线程访问共享变量,为了确保共享变量能够被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值一致。
2.通俗理解:
volatile就是Java的一个关键字,单词volatile本身具有不稳定的意思。volatile关键字表示被修饰的变量的值容易变化,不稳定。volatile变量的不稳定性意味着对这种变量的读和写操作都必须从高速缓存或者主内存中读取,以读取变量相对新的值。
volatile的作用
1.保障读操作、写操作本身的原子性
1. 原理:volatile关键字在原子性方面仅保障对被修饰的变量的读操作、写操作本身的原子性,如果要保障对volatile变量的赋值操作的原子性,那么这个赋值操作不能涉及任何共享变量(包括被赋值的volatile变量本身)的访问。
例子1:num1=num2+1;
如果变量num2也是一个共享变量,那么赋值操作实际上是一个read-modify-write操作。其执行过程中其他线程可能已经更新了num2的值,因此该操作不具备不可分割性,也就不是原子操作。如果变量num2是一个局部变量,那么赋值操作就是一个原子操作。
例子2:volatile Map map =new HashMap();
该操作可以分解为如下伪代码所示的几个子操作:
objRef = alllocate(HashMap.class); // 子操作(1) : 分配对象所需的存储空间 invokeConstructor(objRef); // 子操作(2) : 初始化objRef引用的对象 aMap = objRef; // 子操作(3) : 将对象引用写入变量aMap
虽然volatile关键字仅保障其中的子操作(3)是一个原子操作,但是由于子操作(1)和子操作(2)仅涉及局部变量而未涉及共享变量,因此对变量aMap的赋值操作仍然是一个原子操作.
2.在Java语言中对long型和double型以外的任何类型的变量的写操作都是原子操作。考虑到32位Java虚拟机上对long/double型变量进行的写操作可能不具有原子性。Java语言规范特别的规定对long/double型volatile变量的写操作和读操作也具有原子性。
那么,为什么32位Java虚拟机上对long/double型变量进行的写操作可能不具有原子性呢?
Java中long/double型变量会占用64位的存储空间,而32位的Java虚拟机对这种变量的写操作可能会被分解为两个步骤来实施,比如先写低32位,再写高32位。那么在多个线程试图共享同一个这样的变量时就可能出现一个线程在写高32位的时候,另一个线程正在写低32位。所以最终结果可能就是一个线程对64位的long/double的低32位与另一个线程对该变量的高32位进行更新所混合出来的一个结果。
2.保障有序性
1.原理:Java内存屏障保障了读线程对写线程在更新volatile变量前对共享变量所执行的更新操作的感知顺序与相应的源代码顺序一致,即保障了有序性。
2.JMM如何实现volatile写、读的内存语义:JMM通过限制重排序来保障有序性,重排序分为编译器重排序和处理器重排序。
2.1 JMM限制编译器对volatile重排序
表2-2 JMM针对编译器制定的volatile重排序规则表
举例:对于第一个操作是普通读/写,第二个操作是volatile写,则编译器不能重排序这两个操作。
总结以上表格:
- 当第二个操作是volatile写是,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
- 当第一个操作是volatile读时,不管第二个操作时什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
2.2 JMM限制处理器对volatile重排序
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
下面是基于保守策略的JMM内存屏障插入策略:
2.2-1 volatile写插入内存屏障后生成的指令序列示意图 2.2-2 volatile读插入内存屏障后生成的指令序列示意图
下面是4种屏障作用:
StoreStore屏障:保障上面所有的普通写在volatile写之前刷新到主内存。
StoreLoad屏障:避免volatile写与后面可能有的volatile读/写操作重排序。
LoadLoad屏障:禁止处理器把上面的volatile写与下面的普通读重排序。
LoadStore屏障:禁止处理器把上面的volatile读与下面的普通写重排序。
3.保障可见性
有volatile修饰的共享变量进行写操作时汇编代码会多出Lock指令。
Lock前缀的指令在多核处理器具有以下作用:
1.将当前处理器缓存行的数据写回到系统内存中。Lock前缀指令导致在执行指令期间,声言处理器的LOCK#信号,在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存。
2.这个写回的操作会使其他在CPU里缓存了该内存地址的数据无效。处理器能够使用嗅探技术保证它的内部缓存、系统内存与其他处理器的缓存的数据在总线上保持一致性。
注意:volatile关键字在可见性方面仅仅是保证读线程能够读取到共享变量的相对新值,对于引用型变量,volatile关键字并不能保证线程能够读取到相应对象的字段(实例变量、静态变量)、元素的相对新值。
volatile的变量的开销
volatile的读、写操作都不会导致上下文切换,因此volatile的开销比锁要小。
写一个volatile变量会使该操作以及该操作之前的任何写操作的结果对其他处理器是可同步的,因此volatile变量写操作的成本介于普通变量的写操作和在临界区内进行的写操作之间。
volatile变量读操作的成本也介于普通变量的写操作和在临界区内进行的写操作之间。因为volatile变量的值每次都需要从高速缓存或者主内存中读取,而无法被暂存在寄存器中,从而无法发挥访问的高效性。
volatile的典型应用场景
1.使用volatile变量作为状态标志。应用程序的某个状态由一个线程设置,其他线程会读取该状态并以该状态作为其计算的依据。此时使用volatile的好处是一个线程能够‘通知’另外一个线程某种事件的发生,而这些线程又无须因此而使用锁,从而避免了使用锁的开销。
2.使用volatile保障可见性。在该场景中,多个线程共享一个可变状态变量,其中一个线程更新了该变量之后,其他线程无须加锁的情况下也能够看到该更新。
3.使用volatile变量替代锁。volatile变量并非锁的替代品,但是在一定的条件下他比锁更合适。多个线程共享一组可变状态变量的时候,我们可以把一组可变状态变量封装成一个对象,那么对这些状态变量的更新操作就可以通过创建一个新的对象并将该对象引用赋值给相应的引用型变量来实现。
4.使用volatile实现建议版读写锁。这种简易版读写锁仅涉及一个共享变量并且仅允许一个线程读取这个共享变量时其他线程可以更新该变量。因此,这种读写锁允许读线程可以读取
参考:
《Java并发编程的艺术》
《Java多线程编程实战指南》