volatile详解

volatile的定义与实现原理

在java语言规范第三版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了保存共享变量能够被准确和一致的更新,线程应该确保通过排它锁单独获得这个变量.
java线程内存模型确保所有线程看到这个变量的值是一致的.
在了解volatile实现原理之前,要先看cpu术语与说明 :

  • 内存屏障(memory barriers) 一组处理器指令,用于实现对内存操作的顺序限制
  • 缓冲行( cache line) cpu告诉缓存中可以分配的最小存储单位.处理器填写缓存行时会加载整个缓存行
  • 原子操作: 不可中断的一个或一系列操作
  • 缓存行填充: 当处理器识别到从内存中读取操作数是可以缓存的,处理器就读取整个高速缓存行到适当的缓存

在java中,在对volatile变量进行写操作时候,会有lock指令,此lock指令会使得处理器发生两件事情

  • 将当前处理器缓存行的数据写会到系统内存
  • 这个写回内存的操作会使其他cpu里缓存了该内存地址的数据无效
    在这里为了提高处理速度,处理器不直接与内存直接通信,会首先将系统内存的数据读取到内部缓存后在进行操作.如果是普通变量,操作完不知道什么时候会写会内存,但是如果声明了volatile变量,则会立刻把缓存行的数据写会到系统内存.并且在多处理器下,为了保证各个处理器的缓存是一致的,会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是否过期,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行置为无效.

volatile还有一个功能是禁止指令的重排序.
在程序执行过程中存在三种重排序

  1. 编译器优化的重排序,编译器在不改变单线程程序语义下,可以重新安排语句的执行顺序
  2. 指令级并行的重排序,现代处理器采用指令级并行技术将多条执行重叠执行,如果没有数据依赖性,可以改变语句对应执行的执行顺序
  3. 内存系统的重排序,由于处理器使用缓存和读/写缓冲区,是的加载和存储操作可能是在乱序执行
    1
    2
    3
    4

    int b = 2; 1
    int a = c; 2
    //这里在其他线程看来可能2发生在1的前面,以为这里有缓存一说,所以就有了给b赋值,但是还没有被刷新回主存,但是a=c已经从内存中读取了,在这种情况下,可能会出现内存系统的重排序

volatile对于重排序的规定

当写一个volatile变量的时候,JMM会把该线程对应的本地内存中的共享变量值刷新会主存.当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量

  • 对于volatile写之前的操作不允许放在volatile之后
  • 对于valatile读之后的所有操作不允许放在volatile之前
  • 一个普通变量的写可以被重排序到volatile变量的写之前

在对一个普通变量的单个读/写操作,与一个普通变量的读/写操作都是使用同一个锁同步来说执行效果相同. 这是在jdk1.5之后增强了volatile关键字的语义,这个语义保证了对单个volatile变量的读/写具有原子性.
volatile变量自身拥有如下特性:

  • 可见性: 对一个volatile变量的读,总是能看到对这个变量的最后写入
  • 原子性: 对任意单个变量的读/写具有原子性,但是类似于volatile这种复合操作不具有原子性;

### 内存屏障

  • StoreStore 确保之前的写操作先行与之后的写操作
  • LoadLoad 确保之前的读操作先于之后所有的读操作
  • LoadStore 确保之前的读操作先行与之后所有的写操作
  • StoreLoad 确保之前的写操作先行与之后所有的读操作

JMM在会在使用了volatile的变量使用之前之后加入不同的内存屏障来禁止指令的重排序.保证了单个变量的原子性与可见性.

  • 在每个volatile写操作之前插入一个storeStore屏障
  • 在每个volatile写操作之后插入一个StoreLoad屏障
  • 在每个volatile读操作之前插入一个LoadLoad屏障
  • 在每个volatile读操作之后插入一个LoadStore屏障