Java关键字:深入剖析Volatile关键字

一、volatile的语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义

  1. 保证了不同线程对这个变量进行操作时的可见性。即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
  2. 禁止进行指令重排序

先看一段代码,假如线程1先执行,线程2后执行:

//线程1
boolean stop = false;
while(!stop){
     doSometing();
     }

//线程2
stop = true;

上述代码常用于作为中断线程的标记方法。
实际上,在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(可能性很小,发生则会出现死循环)
无法中断情况:每个线程运行过程中都有自己的工作内存,线程1运行时,会将stop变量的值拷贝一份放到自己的工作内存中;那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中之时,线程2就转去做别的事情了;由于线程1不知道线程2对stop变量的更改,因此还会一直循环下去。
上述情况可以使用volatile修饰,volatile关键字会强制将修改的值立即写入内存

  • 使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是cpu的L1或L2缓存中对应的缓存行无效)
  • 由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取
  • 那么在线程2修改stop值(两个操作:修改值+写入内存)时,会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效哎,它就会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值
    那么线程1读取到的就是最新的正确的值。

(一)volatile与原子性

volatile关键字保证了操作的可见性,但是volatile能保证对变量的操作是原子性的吗?

public class Test{
    public volatile int inc = 0;
    public void increase(){
         inc++;
         }
    public static void main(String[] args){
         final Test test = new Test();
         for(int i=0;i<10;i++){
             new Thread(){
             public void run(){
                for(int j=0;j<1000;j++)
                   test.increase();
             };
             }.start;
         }
         while(Thread.activeCount()>1) //保证前面的线程都执行完
             Thread.yield();
         System.out.println(test.inc);
}
}

上面程序输出结果并非10000。实际上每次运行的结果都不一致,都是一个小于10000的数字。此处存在一个误区,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。

可见性只能保证每次读取的是最新的值,但volatile没办法保证对变量操作的原子性
自增操作是不具备原子性的,包括了读取变量的原始值、进行加1操作和写入工作内存三个子操作。那么就是说这三个子操作可能会分割开执行,就有可能出现下面这种情况:

  • 加入某个时刻变量i的值位10,线程1对变量进行自增操作,线程1读取了变量i的原始值,然后线程1就被阻塞了;
  • 线程2对变量也进行自增操作,线程2去读取变量i的原始值,此时由于线程1只是对变量i进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量i的缓存行无效,所以线程2会直接去主存读取i的值,发现i的值是10,然后进行加1操作后,把11写入工作内存,最后写入主存;
  • 然后线程1接着进行加1操作,由于已经读取了i的值,注意此时在线程1的工作内存中i的值仍然是10,所以线程1对i进行加1操作后值位11,然后将11写入工作内存,最后写入主存。
    综上,两个线程分别进行一个自增操作后,i只增加了1。

原因:线程1对变量进行读取操作之后,被阻塞了的话,并没有对i值进行修改。然后虽然volatile能保证线程2对变量i的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。
即,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作是原子性的。
解决方法:提供原子性的自增操作

:在Java1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的自增、自减、加法操作以及减法操作进行了封装,保证这些操作是原子性操作。atomic是利用CAS(Compare And Swap)来实现原子性操作的,CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作

(二)volatile与有序性

volatile能禁止指令重排序,所以volatile能在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:

  • 当程序执行到volatile变量的读或写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行
  • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行
    举例:
//x、y为非volatile变量;flag为volatile变量
x = 2; //①
y = 0; //②
flag = true; //③
x = 4; //④
y = -1; //⑤

说明:

  • 由于flag变量为volatile变量,那么在进行指令重排序过程的时候,不会将语句③放到①、②前面,也不会将语句③放到④、⑤后面。但是注意①与②的顺序、④和⑤的顺序是不作任何保证的
  • volatile关键字能保证执行到语句③时,①和②是必定执行完毕了的,且①和②的执行结果对③、④、⑤是可见的

二、volatile的原理和实现机制

volatile是如何保证可见性和禁止指令重排序的呢?
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令
lock前缀指令实际相当于一个内存屏障(内存栅栏),内存屏障会提供3个功能

  • 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令前,其前面的操作已经全部完成
  • 强制将对缓存的修改操作立即写入主存
  • 若是写操作,它会导致其他CPU中对应的缓存行无效
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值