volatile关键字浅析

来源:互联网 发布:手机淘宝电脑版在哪里 编辑:程序博客网 时间:2024/06/06 20:25

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 doubleall assignments of referencesall operations of java.concurrent.Atomic* classesall assignments to volatile longs and doubles

volatile修饰的变量仅仅能保证简单的赋值和读取的原子性,并不能保证复杂操作的原子性,如下:

     private volatile int i=0;     ...     f(){          i++;          ...     }

i++这个并不具有原子性,因为i++会转变成好几条指令执行,1)读取当前的值,2)自增,3)存储。这些指令之间随时都有可能被中断,是不安全的。

以上为volatile的简单理解。

0 0
原创粉丝点击