浅谈volatile变量的理解

原文    本文为菠萝大象原创,如要转载请注明出处。http://www.blogjava.net/bolo

Java并发编程里面,volatile是个很重要的概念,大象也来讲讲自己对它的理解。

     以前曾经有段时间我一直没搞明白 volatile 到底怎么用,它是怎样实现的同步,而且对于 volatile 变量还有一些限制条件。任何技术在没完全弄明白之前,至少在没熟练掌握之前都不太敢放心大胆的用,大象想将自己对它的理解分享出来,给需要的人一些帮助。
     volatile 是轻量级的锁,它只具备可见性,但没有原子特性。用 volatile 声明的变量,它的同步特性,简单来讲就是对该变量的单个读 / 写是同步的。这是什么意思呢?我还是以共享变量 i 为例,不过在 i 的前面加上了 volatile 修饰符。
     private   volatile   int   i  = 0;
     public   int  get() {
          return   i ;
     }
     public   void  set( int  i) {
        this.i = i;
    }

     为了与传统的 getXXX setXXX 方法区别开来,我将方法名改成了上面这样。对于 get set 方法,如果有多个线程同时访问, volatile 是可以保证 i 的原子性的,再简单点讲,对于变量 i get set 方法是同步的。这是通过什么来保证的呢?是通过 Java 语言规范 : 如果一个字段被声明为 volatile Java 内存模型确保所有线程看到这个变量的值是一致的。
     看到这里,可能有的童鞋会想,既然 volatile 可以保证内存可见性,那不就解决了浅谈 Java 共享变量这篇文章里面讲到的共享变量的并发问题吗?只要在 i 的前面加上 volatile 就可以解决同步问题了,你确定?实践是最好的办法,动手试下,看看结果如何。
     事实证明这个办法行不通,为什么呢?原因出在 i++ 上面,增量操作符 ++ 不是原子的。这个操作分解开来看是先从堆内存中获得 i 值的副本放到缓存中,然后对副本值加 1 ,最后再将副本值写回到堆内存的变量 i 中。从这个过程我们可以看到,从堆内存中获得 i(get 方法 ) 以及将值写回到 i(set 方法 ) 这两步都是同步的,但中间的就不能保证是同步的了。
     对于 volatile 修饰的变量,只保证了他的可见性,但不保证原子性。最常用的应该是 boolean 类型,它用来作为状态标志,因为它只有 true false 两个值,不会有非原子性的操作。当然不是说只能用在布尔类型变量上面,其它的基本类型和对象类型都可以用。但一定需要小心谨慎的处理,以免掉进并发陷阱而不知。比如 volatile 变量就不适合用于不变性条件这种情况,以上下限为例, lower 必须小于 upper ,这就是一种不变性条件,你可以理解为这是一种规则限制。它们都只有 set get 方法,但在 set 方法里面加入了约束条件,这时, volatile 的可见性就不能保证并发时, lower upper 之间的不变性条件 (lower<upper) 一定成立了。

     /*
     * volatile 只保证 lower upper 的最后写入一定会被其它读取的线程看到
      但不能保证在 lower upper 写入时,另一个变量的值没有发生变化
      */
     private   volatile   int   lower ;
     private   volatile   int   upper ;

     public   int  getLower() {
        return lower;
     }

     public   int  getUpper() {
        return upper;
     }

     public   void  setLower( int  lower) {
          if  (lower >  upper )
               throw   new  IllegalArgumentException();
        this.lower = lower;
     }

     public   void  setUpper( int  upper) {
          if  (upper <  lower )
               throw   new  IllegalArgumentException();
        this.upper = upper;
    }

     如果 lower upper 的初始值为 0 10 ,同一时刻,线程 1 调用 setLower(8) ,线程 2 调用 setUpper(2) ,执行上完全没问题,但是现在的 lower upper 的值就变为了 8 2 这种无效的数据了,所以 volatile 只能确保可见性,不能确保原子性。
     所以在使用 volatile 变量时,请考虑是否满足下面这样的要求:
         1 、对变量的写入操作不依赖变量的当前值 (i++ 这种操作就不行 )
         2 、没有用于其它变量的不变式条件中 (lower<upper)
     到这里,我们已经明白了:用 volatile 修饰的变量只具备可见性,那么它是怎么保证可见性的呢?
     现在大家用的电脑 CPU 基本上都是多核的,至少两核,缓存也有很多级 (L1 L2 L3) 。代码在 JVM 里面执行的时候, JVM 如果发现有 CPU 在处理 volatile 变量的写入操作,就会告诉该 CPU 将当前缓存中的数据写回到堆内存中,但这时其它 CPU 的缓存值还是旧的,再执行操作就会有问题,所以在处理器的内部实现了缓存一致性协议,当有缓存中的数据写回内存时会引起其它 CPU 里这个 volatile 变量的缓存值无效,如果这时候其它 CPU 要想使用就必须到堆内存中重新读取该值,这样就实现了 volatile 变量的可见性。我这样讲方便大家理解,实际的情况比这复杂的多。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值