前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于volatile的坑

关于volatile的坑

作者头像
PhoenixZheng
发布2018-08-07 16:34:41
5060
发布2018-08-07 16:34:41
举报

Java的面试基础问题中,经常出现并发相关的问题。比如volatile关键字,是出现频率相当高的一个问题。 如果说volatile和synchronized的区别,volatile能不能代替synchonized,不知道你是否了解?

volatile关键字并不是万能的

volatile是相对于synchronized轻量级的同步关键字。它所能保证的功能比 synchonized少很多。回忆一下同步的三个要素是什么? · 原子性 · 有序性 · 可见性 对于 synchonized来说,这三个要素都是保证的,而 volatile只能保证有序性和可见性。这样会带来什么问题呢,比如我们看看下面这段代码。

代码语言:javascript
复制
public class VolatileDemo {

  public static volatile int count = 0;
  public static void increase() {
  //为了效果明显这里增加延时
    try {
      Thread.sleep(10);
    } catch (Exception e){}
    count++;
  }
  public static void main(String[] args) {

    for(int i = 0;i < 100; i ++) {
      new Thread(new Runnable() {
        @Override
        public void run() {
          VolatileDemo.increase();
        }
      }).start();
    }
    //延时足够长的时间等待所有线程完成
    try {
      Thread.sleep(2000);
    } catch(Exception e) {}

    System.out.println("count: " + count);
  }
}

如果把volatile关键字去掉的话,这段代码输出结果肯定不是100.但如果加上 volatile呢?

$ java VolatileDemo count: 91

结果也不为100。出现这个问题的原因就要回到并发三要素了。

volatile 的局限

上面说过, volatile只满足三要素的有序和可见,不满足原子性。看上面的代码,

代码语言:javascript
复制
count++

这段代码包含三个阶段,读->改->写入内存,一个完整的并发安全操作,首要必须满足原子性,意味着当读操作发生时,应该是阻塞的,其他线程不能打断当前操作。volatile不满足原子性,因此当线程2读count时,线程1早已把count的值读进缓存中,那么可以理解此时线程1和2中的count值是相同的。在各自修改数据后,线程1会把count值写回公共内存,虽然 volatile的可见性保证了在写入之后,其他CPU缓存中的值失效,我们以为其他线程应该会再去读最新的值,但是此时已经读取过count值的线程不会再去读取最新的count值,这导致线程2并没有在最新的值上做修改,所以导致这个问题。`

所以对于这种需要保证原子性的操作来说,用volatile关键字是不行的,得用 synchonized。 说个题外话,看下面几个操作,哪些可能不是原子操作呢?

代码语言:javascript
复制
int x,y;
long time;
x = 1; //1
y = x; //2
time = 1522048997021; //3

结果是,除了1之外2和3都不是原子操作。1和2好理解,因为单纯的读,是原子操作,读->写就不是原子操作了。 然而3为什么不是原子操作呢? 在java中,long是64位值,在某些32位系统上,对64位数据的写需要分成两次32位的写操作,因此对long的写就可能不是原子操作了。这种问题其实在面试中经常被拿来挖坑…要多注意。

volatile的用途

回到 volatile,它的使用需要同时满足两个属性, · 对变量的写操作不依赖于当前值 · 该变量没有包含在具有其他变量的不变式中 对于第一个情形,像上面的 count++就是不满足的,虽然看起来只是一个自增操作,但实际上包含了读改写,就意味着当前值可能已经被别的线程串改了。 而对于第二个条件,字面意思不好理解,可以参考下面的代码,代码中包含一个不变式,lower < upper,即使我们把 lower和upper设定为 volatile,仍然会发生两个线程同时分别执行 setLower和 setUpper,导致区间变成类似 [3,4]的情况。

代码语言:javascript
复制
public class NumberRange {
    private int lower, upper;

    public int getLower() { return lower; }
    public int getUpper() { return upper; }

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

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

对于上面的代码,优化的唯一方法是把两个set方法改为 synchonized。

总结

volatile 只满足并发的可见性和有序性,对于需要保证原子性的场景则只能用 synchonized关键字。 通常用 volatile的是标志变量如

代码语言:javascript
复制
boolean volatile flag;

还有双重检查锁定的单例类实例对象中。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-04-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android每日一讲 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • volatile关键字并不是万能的
  • volatile 的局限
  • volatile的用途
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档