深入并发关键字-volatile

仅供学习交流,如有错误请指出,如要转载请加上出处,谢谢

前言

关键字volatile可以说是JVM提供的最轻量的同步机制了,也被称为轻量级的 synchronized,他在多处理器并发编程中提供了两个重要的特性:

  1. 保证共享变量的可见性,指一个线程修改了一个共享变量的值,其他线程能够读取到最新的修改值
  2. 禁止重排序,指禁止代码在执行过程中为优化性能而编译的执行顺序

如果在合适的情况下使用volatile关键字,程序会更加的高效,因为他对于synchronized来讲不会使线程上下文的调度和切换

volatile的实现原理

volatile是如果实现可见性和禁止指令重排序的?先通过一段双重检查单例模式(double checked singleton)代码开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DoubleCheckedSingleton {
private volatile static DoubleCheckedSingleton instance;
private DoubleCheckedSingleton (){}
public static DoubleCheckedSingleton getInstance() {
if (instance == null) {//single checked
synchronized (DoubleCheckedSingleton.class) {
if (instance == null) {//double checked
instance = new DoubleCheckedSingleton();
}
}
}
return instance;
}
}

在上述代码中,由volatile修饰的赋值代码片段instance = new DoubleCheckedSingleton();在x86处理器下通过工具获取JIT编译器生成的汇编代码指令如下所示:

1
2
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);

相对于普通变量的赋值操作来讲,volatile修饰的共享变量的在赋值后多执行了lock addl $0x0,(%esp)指令操作,其中lock指令前缀在多核处理器中会做两件事:

  1. 将当前CPU缓存行的数据写回到主内存;
  2. 这个写回内存的操作会导致在其它CPU里缓存了该内存地址的数据无效。

处理器为了更加高效的运行,他不会直接与主内存进行通信,而是在先读取到其处理器内部缓存进行一系列的操作,但是不是立刻会回写到主内存中,对于volatile修饰的共享变量进行写操作,JVM会向该处理器发送一条Lock前缀的指令,锁住该缓存(早期的处理器是通过锁住整个总线,效率较低),并使用缓存一致性来确保修改的原子性,该操作称为“缓存锁定”,然后引起该处理器将内部缓存回写到主内存中,在确保缓存一致性的情况下,处理器通过嗅探技术访问主内存和内部缓存,确保处理器内部缓存和主内存上的状态保持一致,如果一个处理器在内部缓存对一个共享地址进行写操作时,该处理器会无效其对应的缓存,再下次访问该内存地址时,进行读取主内存到该处理器内部缓存。

lock前缀指令也相当于一个内存屏障(Memory Barrier),如上所述,由于缓存锁定操作,这样让该指令执行完,下一个指令才能执行,这个就实现了禁止了指令重排序,回写主内存同时使其他cpu该内存地址无效化,这样也就实现了可见性。

volatile关键字的使用

如何使用volatile关键字,这里要了解确保并发安全的三个要素:原子性,可见性,顺序性,其中原子性指该操作不可分割,不受其他线程干扰,顺序性就是禁止指令重排序,对于关键字volatile来讲,volatile关键字保证了可见性和顺序性,同时它也是一个轻量级的synchronized,它在多处理器上的安全性体现在更加细微的操作,上述也提到,由于缓存一致性会保证单个读写操作的原子性,这样就符合并发安全的三要素了,然而在大多数程序中,大多数都是对一些共享变量的复合操作,比如i++操作,这就涉及到从主内存读取i到缓存中,在缓存中进行i+1的操作和将i写入主内存三个操作,这个就无法保证i++操作的原子性了,对于类似的复合操作来讲,如果共享变量具有原子性或者在一些原子操作的场合下,比如共享变量被concurrent包下的一些原子类修饰或者做一些CAS的一些操作,使用volatile是一个好的选择。

参考

http://ifeve.com/volatile/

周志明《深入理解java虚拟机》第十二章