java面试题网站:www.javaoffers.com
volatile关键字我们在开发的时候很少使用,在看spring源码的时候很有可能会看到,但是很少有人知道在什么环境下使用。本人查看一些书籍资料后做一下总结。

volatile 通常理解为jvm虚拟机提供的最轻量级的同步机制(本句来源于‘深入理解JAVA虚拟机’)
两大特征
(1)使用volatile的变量对所有线程具有可见性。
(2)使用volatile的变量禁止指令重排序
两大特征的具体理解:
可见性:
   当volatile变量的值被修改后,其他线程可立即得知。而普通的变量却做不到,区别在于
     1:volatile变量在修改完成之后会从cache(cpu的高速缓存区)立即直接刷回RAM(主内存),普通变量并不会立即刷新到RAM。
     2:每次使用volatile变量时都是从RAM获取。而普通变量有可能是从Cache中
     3:volatile变量从Cache刷新到RAM后,会通知其他CPU或别的内核其Cache无效,这样会导致其他CPU或内核在使用其volatile变量时需要重新从RAM读取
     在这里插入图片描述

禁止指令重排序:
   按照编写代码的顺序执行,不会进行代码优化,尽管cpu保证指令重排序后当前线程的执行结果具有正确性,但是在多线程下会出现问题,举例来说:

     共享变量 B: static boolean B = false;
   线程1:
               doSome();//处理一些事情
               B = true; //将B的状态改为True;
                上面的代码如果进行了指令重排序后为(最终结果没有改变,均为处理一些事和B状态改为True):
               B = true; //将B的状态改为True;
               doSome();//处理一些事情
   
   线程2:
               while(true){
                        if(B){
                        getSome();//获取线程1中doSome的结果。
                        break;
                 }
               }  

在多线程(并发)运行的环境下(没有禁止指令重排序),那么线程1中的共享变量B有可能先执行,这样会导致线程2中的getSome();//提前执行,并且很有可能不能够获取线程1中doSome执行的结果。所以指令重排序只能保证当前线程执行的正确性,但是不能保证在多线程并发的情况下执行的正确性,因为cpu不能够知道两个线程之间的存在逻辑关系。因此在这种情况下我们可以使用volatile关键字来禁止指令重排序
   
可见性和禁止指令重排序实现的原理(内存屏障):
举例代码:

  public class Singleton{
    private volatile static Singleton instance;
    public static Singleton getInstance(){
      if(instance==null){
        synchronized(Singleton.class){
         if(instance==null){
           instance=new Singleton();
         }
        }
      }
      return instance;
     }
    public static void main(String[]args){
    Singleton.getInstance();
    }
  }  

编译后,这段代码对instance变量赋值部分如代码清单下所示。

0x01a3de0f:mov$0x3375cdb0,%esi;......beb0cd75 33
;{oop('Singleton')}
0x01a3de14:mov%eax,0x150(%esi);......89865001 0000
0x01a3de1a:shr$0x9,%esi;......c1ee09
0x01a3de1d:movb$0x0,0x1104800(%esi);......c6860048 100100
0x01a3de24:lock addl$0x0,(%esp);......f0830424 00
;*putstatic instance
;-
Singleton:getInstance@24
 有volatile修饰的变量,赋值后(前面mov%eax,0x150(%esi)这句便是赋值操作)**多执行了一个**“**lock addl $0x0,(%esp)**”操作,**这个操作相当于一个内存屏障(Memory Barrier或Memory Fence**).

该操作(内存屏障)“addl $0x0,(%esp)”查询IA32手册得知:
1:重排序时不能把后面的指令重排序到内存屏障之前的位置,所以lock addl$0x0,(%esp)指令把0(空)同步到内存时并告知其他cpu的缓存失效,并且使用到了lock 保证总线是串行,这样就形成了“指令重排序无法越过内存屏障”的效果。
2:使当前CPU的Cache写入了内存并且该写入动作也会引起别的CPU或者别的内核无效化(Invalidate)其Cache。所以通过这样一个操作,可让volatile变量的修改对其他CPU立即可见,因为其他的CPU的Cache失效后需要重新读取。

小记volatile对64位的long和double的读取具有原子性
   java内存模型中特别定义了一条相对宽松的规定:Java内存模型要求lock、unlock、read、load、assign、use、store、write这8个操作都具有原子性,但是允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行(有的话64位数据读取具有原子性,可以理解为一次读),即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这4个操作的原子性,这点就是所谓的long和double的非原子性协定在实际开发中,目前各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不需要把用到的long和double变量专门声明为volatile

参考资料:深入理解JAVA虚拟机->第十二章 -> 12.3.3 对于volatile型变量的特殊规则

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐