浅析volatile关键字

一、特性:

volatile是Java中的关键字,用来修饰会被不同线程访问和修改的变量。JMM(Java内存模型)是围绕并发过程中如何处理可见性、原子性和有序性这3个特征建立起来的,而volatile可以保证其中的可见性和有序性。

1、可见性

未被volatile修饰的情况:

//线程1
 
boolean stop = false;
 
while(!stop){
 
    doSomething();
 
}
 
 
 
//线程2
 
stop = true;

如上伪代码所示,当线程二读取内存中的之后如果线程二去做了其它事情,而没有把修改的信息写到主存中的话,线程一就会一直在循环中无法退出。

被volatile修饰的情况:

//线程1
 
boolean volatile stop = false;
 
while(!stop){
 
    doSomething();
 
}
 
 
 
//线程2
 
stop = true;

而被volatile修饰后就不一样了,程序会在线程二修改完变量后强制线程二将数据写到主存中,此时线程一就会停止运行。

2、原子性

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的数字。

  可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。

  这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

  在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

  假如某个时刻变量inc的值为10,

  线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

  然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

  然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

  那么两个线程分别进行了一次自增操作后,inc只增加了1。

  解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。

  根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

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

二、底层解析

volatile的底层是基于JRS软件层面的屏障,JRS屏障的原理详见这篇文章(有序性保障:X86 CPU内存屏障和JSR内存屏障_循环网络不循环的博客-CSDN博客),实现的原理图如下。

 

参考链接:

volatile详解 - 钟齐峰 - 博客园

Volatile详解,太详细了 - Code2020 - 博客园

volatile详解_森林屿麓的博客-CSDN博客_volatile

volatile 关键字,你真的理解吗? - 知乎有序性保障:X86 CPU内存屏障和JSR内存屏障_循环网络不循环的博客-CSDN博客volatile 关键字,你真的理解吗? - 知乎

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小海海不怕困难

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值