volatile关键字的作用及使用场景

volatile关键字是java虚拟机提供的最轻量级的同步机制。在并发的场景下,我们都习惯于使用synchronized锁进行同步,其实,有些简单的场景下,我们也可以使用volatile来代替,但volatile有它本身的使用规则,不能滥用,要结合它自身的特性把它应用于适合的场景下。下面就来简单叙述一下volatile关键字的作用,及其适用场景。

一、volatile的特性

被volatile修饰的变量将会有两个特性,第一是保证此变量对所有的线程的可见性;第二是禁止指令重排。

1、可见性

这里的可见性,指的是一条线程修改了被volatile修饰的变量的值后,新值对于其他线程也是立即得知的。
如下图,当某时刻线程T1对变量V作了B操作,那么线程T2、T3也可以马上感知到V被作了B操作。
在这里插入图片描述
熟悉jvm内存模型的童鞋,应该知道变量(不包括局部变量和方法变量)都是存在于主内存中,而每个线程对此变量的所有操作都必须通过工作内存,工作内存会不断的从主内存读取数据,或往主内存写入数据。在多线程对同一个变量进行操作时,会存在“可见性”问题。而volatile关键字来修饰变量,就是使得线程每次使用变量时都会强制先从主内存刷新到工作内存,每次修改变量时都会强制立刻把工作内存中发生的改变回写到主内存中。这样就实现了“当一个线程对变量操作时,其他线程都可以感知到”这样的说法。
那么,volatile就一定是线程安全了吗?然而并不是,volatile只是增强了可见性,但都是有前提的。对一个变量的操作,其实很多都不是原子操作,比如一个整型全局变量a,a++这样的操作就不是原子操作,它可以拆解成3条指令:

1、从主内存取出变量a的值,load到工作内存中;
2、工作内存中的a值加1;
3、工作内存中新的a值回写到主内存。

由此可见,就算volatile保证了变量的可见性,但在多线程的环境下,依然保证不了线程安全,因为多线程的读取、写入总会存在差异,此时应该使用原子类乃至锁机制方可解决。(以上的例子不是很严谨,因为一条高级语言指令也是由很多条机器指令完成)

2、禁止指令重排

CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令重排。指令重排在单线程是没什么问题的,但如果是多线程的情况下,就有可能产生意想不到的现象。如下程序:

class UnsafeExam {
  int x = 0;
  boolean v = false;
  //volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里 x 会是多少呢?
      System.out.print(x);
    }
  }
}

由以上程序,如果编译器优化的时候,把x = 42和v = true换了个位置,在多线程的环境下,后果是不堪设想的,x的值输出可能是0或42。如果变量v加个volatile修饰,相当于加了个内存屏障,指令重排时不能把后面的指令重排序到内存屏障之前的位置。

二、性能

volatile变量读操作的性能消耗与普通变量差别不大,单是写操作可能会慢一些,因为他需要在本地代码插入许多内存屏障指令来保证处理器不发生乱序执行。不过即便如此,大多数场景volatile关键字的总开销要比锁来的更低。我们在volatile与锁中选择的唯一判断一句仅仅是volatile语义能否满足使用场景的需求。

三、适用场景

由于volatile只能保证可见性,则使用时必须遵守以下原则,否则需要考虑使用原子类乃至锁机制。

  1. 运算结果不依赖变量的当前值,或者能确保只有单一的线程修改变量的值;
  2. 变量不需要与其他状态变量共同参与不变约束。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章