volatile变量应用详解

应用场合及原理分析

随着多核CPU的普及,多线程的并发处理数据成为了人们提高程序速度的必要手段。但由于CPU中的高速缓存机制的影响,多线程对于数据的操作会出现数据读写不一致的情况,在JVM虚拟机中也是采用了同样的机制进行多线程的运行,对每个线程分配一定量的运行内存使其完成操作任务。JVM对数据的操作是分为三步的,例如下面这样的代码:

//线程1
int i = 0;
i++;

//线程2
i = i + 2;

这样的代码在CPU内核中,会先从主存中把i的值0取出来,然后在高速缓存中完成自增1的运算,最后回存入主存中。
因此在语言实现的过程中,必须有一种或多种对策来防止上述情况的发生。在java的语言实现中,提供了九种防止多线程的步伐不一致的对策:

  1. Synchronized
  2. CountDownLatch
  3. CyclicBarrier
  4. Exchanger
  5. Semaphore
  6. Phaser
  7. ForkJoinPool
  8. SwingWorker
  9. volatile变量

本文将介绍最后一种——volatile变量。
这种声明方法有两种作用同时也是实现原理:

  1. 保证不同的线程所看到的同一个变量的一致性。一旦某个线程更改了变量的值,别的线程会立即看到这个变量的新值。
  2. 禁止了指令的重排序。

比如下面这个例子:

//线程1
int i = 1;
while(i == 1)
{
    stop();
}

//线程2
i++;

这个例子是用来实现中断进程的。但是这样的写法因为没有保护进程的操作,在线程1还在while循环中休眠时,线程2已经完成了对i的自增操作,这时线程1中运行内存里的i值还是1,它还在休眠,这样的程序就没有达到编写者的意图。如果对i变量加上了volatile声明如下:

//线程1
int volatile i = 1;
while(i == 1)
{
    stop();
}

//线程2
i++;

这样,在线程中运行的变量就有点不一样了,会按顺序发生如下变化:

  1. 线程1和2将i=0读入自身的运行内存中。
  2. 线程1进入休眠
  3. 线程2完成对i的自增,i=2被立即写入主存中,同时使线程1运行内存中的缓存变量i=1无效。
  4. 线程1因为自身内存中的临时变量无效,就去主存中读取i的值,这时读到了i=2的最新值,因此结束休眠。

以上volatile关键字能够完成的,但是JVM里面有一个重要的原子性操作(原子性操作指对变量进行赋予真数字这样不能中断的操作)是volatile关键字不能完成的。比如这样一个程序:

public class test{
    public volatile static int i=0;
    public static void main(String[] args) {
        for (int k = 0; k < 10; k++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    i++;
                }
            });
            thread.start();
        }
        System.out.println(i);
    }
}

按照大众的期待,最后输出的结果应该是1000,可是这个程序的结果极小可能会是1000,原因正是volatile关键字不能保证原子性操作。假设一个线程A被创建出来,此时它已经有1个兄弟B在工作了,而i=10。前文说到过,JVM中对数据的操作分为三步:提取,改变,回存。若B已经提取到了i的值10而正在改变(自增1),A又去提取i的值10。这时,B的动作是比A提前的,因此会比A先回存i=11的值,但是A后来的动作又会把i=11的值再次回存一遍。这里,A和B的操作都是符合volatile的规范的,既没有隐瞒自己改数据的行为,也没有改变程序顺序。但是两步自增操作却使i只增加了1。这便是volatile不能实现原子性操作的原因。

实际应用举例

那么,volatile变量在java中有什么实际应用呢?

  • 可以在多线程并发的处理环境中标志公用变量
volatile boolean shutdownRequested;

public void shutdown() { shutdownRequested = true; }

//线程1
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}
//线程2
public void shutdownAfterTimeout()
{
    while(timeout)
    {
        shutdownRequested = false;
    }
}

如上述例子,如果一个变量在设计思路中需要同步的影响到多个线程,这个变量可以声明为volatile变量来简便地实现使此变量被多个线程同步的监听。

  • 一次性安全发布
public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;

    public void initInBackground() {
        // do lots of stuff
        theFlooble = new Flooble();  // this is the only write to theFlooble
    }
}

public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // do some stuff...
            // use the Flooble, but only if it is ready
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}

如果 theFlooble 引用不是 volatile 类型,doWork() 中的代码在解除对 theFlooble 的引用时,将会得到一个不完全构造的 Flooble。

总结

volatile关键字是需要同步通知所有线程,又因内存开销的原因不便使用同步块的场合下的最优选。可以有效降低程序的内存使用,作为优化代码的一种常用的策略。

参考文献:

  1. Brian Goetz 《正确使用 Volatile 变量》https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
  2. 海子 《Java并发编程:volatile关键字解析》http://www.importnew.com/18126.html

作者:151142429xiehang

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值