已复制
全屏展示
复制代码

Java中的volatile关键字原理


· 4 min read

一. 概述

在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题,被volatile关键字修饰的变量(类的成员变量、类的静态成员变量),具有了可见性、有序性,但是不能保证原子性。

二. 可见性

volatile关键字保证了多线程间的可见性。

被volatile修饰的变量的读取和修改特征:

  • 每次访问变量时,总是获取主内存的最新值;
  • 每次修改变量(线程缓存)后,立刻回写到主内存。

三. 有序性

volatile关键字禁止指令重排序,即有序性。

有两层意思:

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经完成,且结果已经对后面的操作可见了,在其后面的操作肯定还没有进行。
  • 在进行指令优化时,不能将volatile变量前的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

举例说明

// x y为非volatile变量
// flag为volatile变量
 
x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;        //语句4
y = -1;       //语句5

由于flag变量为volatile变量,那么在进行指令重排序的时候,不会将语句3放到语句1、语句2前面,也不会将语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

四. 原子性

volatile关键字不保证原子性。

Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。

一个变量i被volatile修饰,假设两个线程A、B想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步:

  • 首先获取i的值。
  • 其次对i的值进行加1。
  • 最后将得到的新值写会到缓存中和主存中。

更新操作步骤:

  • 线程A首先得到了i的初始值100,但是还没来得及修改,就出现阻塞了。
  • 这时线程B开始了,它也得到了i的值,由于i的值未被修改得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。
  • 线程A阻塞结束后,然后加1得到101,再将值写到缓存,最后刷入主存。所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。

五. ABA问题

假如线程1使用CAS修改初始值为A的变量X(X=A),那么线程1首先会获取当前变量X的值(A),然后使用CAS操作尝试修改X的值为B,如果使用CAS修改成功了,那么程序运行一定是正常的吗?其实未必,这是因为有可能在线程1获取到变量X的值A后,在执行CAS之前,线程2使用了CAS修改了变量X值为B,然后又使用了CAS操作使得变量X值为A,虽然线程A执行了CAS操作时X=A,但是这个A已经不是线程1获取到的A了。这就是ABA问题。ABA问题的产生是因为变量的状态值产生了环形转换,就是变量值可以从A到B,也可以B到A,如果变量的值只能朝着一个方向转换,例如A到B,B到C,不构成环路,就不会存在这个问题。JDK中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题。

🔗

文章推荐