关于volatile的坑

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

volatile关键字并不是万能的

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

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只满足三要素的有序和可见,不满足原子性。看上面的代码,

count++

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

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

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]的情况。

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的是标志变量如

boolean volatile flag;

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

原文发布于微信公众号 - Android每日一讲(gh_f053f29083b9)

原文发表时间:2018-04-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏coding for love

在线商城项目08-数据库创建和商品集合的创建

因为six_tao中没有任何内容。我们需要为其创建一个集合或者插入文档,数据库才会显示。例如:

774
来自专栏编程

PHP7 下的协程实现

前言 相信大家都听说过『协程』这个概念吧。 但是有些同学对这个概念似懂非懂,不知道怎么实现,怎么用,用在哪,甚至有些人认为yield就是协程! 我始终相信,如果...

3578
来自专栏信安之路

二进制漏洞学习笔记

这个程序非常简单,甚至不需要你写脚本,直接运行就能获得shell。 写这个程序的目的主要是为了使第一次接触漏洞的同学更好地理解栈溢出的原理。

1390
来自专栏码农二狗

防止因事务未提交导致的死锁

1054
来自专栏magicsoar

C++编译与链接(2)-浅谈内部链接与外部链接

发现每次写技术博客时,都会在文章开头处花费一番功夫 ...从前,有一个程序员....他的名字叫magicsoar 为什么有时会出现aaa已在bbb中重定义的错误...

2899
来自专栏不会写文章的程序员不是好厨师

伪共享(False Sharing)和缓存行(Cache Line) 大杂烩

在上篇介绍LongAdder的文章中,我们最后留下了一个问题,为什么Cell中要插入很多个实际上并没有使用的Long变量?这个问题就得从False Sharin...

1521
来自专栏Java后端技术栈

Java多线程编程-(1)-线程安全和锁Synchronized概念

(1)在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单位都是进程。

893
来自专栏Golang语言社区

Go语言基于共享变量的并发

一个特定类型的方法和操作函数是并发安全的,那么所有它的访问方法和操作都是并发安全的。导出包级别的函数一般情况下都是并发安全的,package级的变量没法被限制在...

3964
来自专栏程序员互动联盟

【专业技术】Linux设备驱动第六篇:高级字符驱动操作之iotcl

在之前我们介绍了如何实现一个简单的字符设备驱动,并介绍了简单的open,close,read,write等驱动提供的基本功能。但是一个真正的设备驱动往往提供了比...

3598
来自专栏Golang语言社区

Go语言基于共享变量的并发

一个特定类型的方法和操作函数是并发安全的,那么所有它的访问方法和操作都是并发安全的。导出包级别的函数一般情况下都是并发安全的,package级的变量没法被限制在...

41811

扫码关注云+社区

领取腾讯云代金券