Volatile

什么是volatile

1.Java语言规范第3版中对volatile的定义以下:java

  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是一个局部变量,那么赋值操做就是一个原子操做。spa

例子2:volatile Map map =new HashMap();线程

该操做能够分解为以下伪代码所示的几个子操做:对象

objRef = alllocate(HashMap.class);    // 子操做(1) : 分配对象所需的存储空间
invokeConstructor(objRef);    // 子操做(2) : 初始化objRef引用的对象
aMap = objRef;    // 子操做(3) : 将对象引用写入变量aMap  

虽然volatile关键字仅保障其中的子操做(3)是一个原子操做,可是因为子操做(1)和子操做(2)仅涉及局部变量而未涉及共享变量,所以对变量aMap的赋值操做仍然是一个原子操做.blog

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多线程编程实战指南》

相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息