并发编程volatile关键字分析

java并发编程volatile关键字分析

并发编程中经常会使用一些关键字来确保数据的一致性和正确性,那么volatile究竟是什么,它有哪些用处,带着这些疑问,我们来分析一下。

java内存模型

在了解volatile之前,我们首先要弄清楚java内存模型里的几个概念,原子性,可见性,有序性。
1.原子性
由java内存模型来直接保证的原子变量操作包括read、load、assign、use、store、write,我们大致可以认为基本数据类型的访问读写是具备原子性的。即这些操作是不可被中断的,要么执行,要么不执行。java提供了lock和unlock来确保这种需求,尽管虚拟机没有开放给用户这个操作。但还提供了sychronized关键字,这个关键字修饰的代码是具备原子性的。
2.可见性
可见性就比较好解释了,就是说一个共享变量被一个线程修改了,另一个线程能立马得知这个修改。那么重点来了,本文讲的volatile关键字就具有这种特性。
3.有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

volatile可见性使用场景代码演示

volatile boolean shutdownRequested;
public void shutdown() {

    shutdownRequested = true;
}
public void dowork() {

    while (!shutdownRequested) {
        //do something
    }
}

上述代码确保了,只要一个线程调用了shutdow方法,那么由于shutdownRequested变量是对所有线程立即可见的,所以用volatile控制并发非常合适。大概原理如下:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

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没办法保证对变量的操作的原子性。
由此看来,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也无法保证对变量的任何操作都是原子性的。
如果想实现原子性,使用sychronized或者lockAtomicInteger关键字即可。

猜你喜欢

转载自blog.csdn.net/hightrees/article/details/78703918
今日推荐