volatile关键字浅析
在并发编程中,volatile是很常用的一个修饰符。JDK官方文档是这么形容volatile的:
The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes. A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable.
这里意思很明白,volatile在某些用途下比锁更方便,一个变量被声明为volatile,jvm能够保证所有的线程都看到的是一致的变量,也就说一个线程对一个共享变量修改,其他线程能够立即看到最新的值。
volatile在某些情况下可以代替锁,比synchronized成本和开销更低,因为不会引起线程的上下文切换,为什么上下文切换就会开销比较大,因为原来缓存的指令和数据都没用了,要重新加载指令和数据到缓存中,你说开销大不大。
1.首先看看可见性
volatile修饰的共享变量,当该变量发生写操作时,把该变量刷新到内存中,会使所有的CPU缓存的该共享变量失效。volatile修饰的变量写操作,在x86处理器下JIT编译器生成的汇编指令大致如下:
mov %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier
这里会多出一个lock指令,lock指令会把当前CPU缓存的该行写到内存中,同时使其他CPU的缓存失效。现代多处理器体系中,为了加快执行速度,都会使用缓存(cache),数据载入到缓存中再进行各种操作。lock指令把这个变量所在的缓存行回写到内存中,多处理器缓存之间有一致性协议,这时候发现内存中的值已经修改,其他处理器的缓存会被置为失效,这样就达到了在多CPU之间可见。这里插一句,以前lock实现是通过锁总线,现在是通过锁缓存区域,因此开销降低了很多。
2.再来看看重排
学过计算机体系结构和编译原理都知道,现代计算机都是采用流水线结构,编译器会在不影响执行结果的情况下把指令执行顺序优化,最大程度地发挥计算机各个部件的并行特性。再看上面的汇编指令,lock addl $0x0,(%rsp)这是内存屏障(memory barrier),它除了刷新所有cpu缓存外,还会告诉CPU,先于这个命令的必须先执行,后于这个的必须后执行,确保操作执行的顺序,限制了指令重排。
有了这个,才会有happens-before规则,比如以下典型的happens-before例子;
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if(flag){ //3
int i = a; //4
}
}
假设线程a调用了writer()方法,由于flag是volatile修饰的,禁止了编译器重排,步骤1一定会先于步骤2执行。在reader方法中,如果flag是true,那么这个时候a的值一定是1。当然,happens-before还有很多其他的效果,这里只是一个简单例子。
3.原子性
java中,64位机器上给所有基本类型变量赋值操作都是原子的,但是在32位机器上,long和double类型赋值并不是。long和double类型是8字节,32位机器上赋值是先赋值32位,后赋值32位,中间可能被中断,因此不是原子的。如果把long和double类型用volatile来修饰,即可保证赋值操作是原子的了,根据前面的分析,其实很好理解为什么加了volatile之后赋值就是原子的了,因为jvm编译器会加一个lock前缀的指令,计算机会把对应的缓存区域锁住,其他线程就无法修改了,这样就完成了原子性赋值。
在java中,以下操作是原子的:
all assignments of primitive types except for long and double
all assignments of references
all operations of java.concurrent.Atomic* classes
all assignments to volatile longs and doubles
volatile修饰的变量仅仅能保证简单的赋值和读取的原子性,并不能保证复杂操作的原子性,如下:
private volatile int i=0;
...
f(){
i++;
...
}
i++这个并不具有原子性,因为i++会转变成好几条指令执行,1)读取当前的值,2)自增,3)存储。这些指令之间随时都有可能被中断,是不安全的。
以上为volatile的简单理解。