Android并发编程之图文解析volatile关键字

相信很多人对于volatile关键字既熟悉又陌生,熟悉是对这个名字很熟悉,陌生是对他的原理和用法很陌生,最近几天通过查阅大量资料和书,终于对volatile有了一定的理解,写此博客一来做了记录,二来使大家减少学习成本

volatile为什么不能保证原子性?

现在我们的手机都是多核的,也就是说同时有好几颗CPU在工作,每颗CPU都有自己的Cache高速缓存,因为CPU的速度特别快,而内存的读取操作相对于CPU的运算速度来说很慢,所以就会拖累CPU的效率,引入Cache就是为了解决这个问题的,CPU先把需要的数据从内存中读到Cache中,然后直接和Cache来打交道,Cache的速度很快,因此可以保证CPU的工作效率,当Cache中的数据改变后,再将被改变的数据写回内存中。
这里写图片描述
首先我们分析一下多线程在访问一个普通的(没有加volatile修饰符)的变量的过程
1.CPU1和CPU2先将count变量读到Cache中,比如内存中count=1,那么现在两个CPU中Cache中的count都等于1
2.CPU1和CPU2分别开启一个线程来对count进行自增操作,每个线程都有一块自己的独立内存空间来存放count的副本。
3.线程1对副本count进行自增操作后,count=2 ; 线程2对副本count进行自增操作后,count=2
4.线程1和线程2将操作后的count写回到Cache缓存的count中
5.CPU1和CPU2将Cache中的count写回内存中的count。
那么问题就来了,线程1和线程2操作的count都是一个count副本,这两个副本的值是一样的,所以进行了两次自增后,写回内存的count=2。而正确的结果应该为count=3。这就是多线程并发所带来的问题

如果变量count用volatile修饰了可以解决这个问题吗?

如果一个变量加了volatile关键字,就会告诉编译器和JVM的内存模型:这个变量是对所有线程共享的、可见的,每次JVM都会读取最新写入的值并使其最新值在所有CPU可见。我们再来看一下线程在访问一个加了volatile修饰符的变量的过程
这里写图片描述
当count用volatile关键字修饰后,CPU1对count的值更新后,在写回内存的同时会通知CPU2 count值已经更新了,你需要从内存中获取count最新的值!

注意:这里说CPU1通知CPU2其实是不严谨的,其实这是缓存一致性机制在其作用,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行数据时,会使缓存行无效,当CPU1将新数据写回内存后,会修改该数据在内存中的内存地址,CPU2通过嗅探在总线上传播的数据来检查自己的缓存行对应的内存地址是否被修改,如果被修改则将CPU2的该数据缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到CPU2的缓存行里。其实并不是CPU1通知CPU2,而是CPU2自己去嗅探。

其实大家只要明白了原理,怎么说也无所谓,就像好多地方都说volatile修饰的变量,线程直接和内存交互,不会保存副本。而实际上线程还是会保存副本,只不过CPU每次都会从内存中拿到最新的值,并且改变数据之后立马写回内存,看上去就像线程直接和内存交互一样。

然后CPU2中的线程如果需要使用到count的时候,就会再从内存中读取count的值来更新自己的Cache。这看上去似乎解决了我们的问题,其实问题依然存在,我们来分析一下:
比如当前count=1,CPU1和CPU2的Cache中的count都等于1,CPU1中的线程1对count进行了自增操作,然后CPU1更新了内存中count的值,并且通知CPU2 count的值已经改变,然后CPU2从内存中将count=2读到了Cache中,并且线程2开始执行count的自增操作,而就在CPU2刚刚将count的值读回Cache的时候,CPU1又更新了count的值,此时count=3,并且通知CPU2,但是此时线程2已经开始执行了,线程2已经将count=2拷贝到自己的内存空间中了,所以即使CPU2再次更新自己Cache中的count=3,也来不及了,线程2操作的是他自己内存空间中的count副本,所以线程2给count做完自增操作后,将count=3并且写回Cache,CPU2更新内存中的count。此时count的值应该是4,然而CPU2更新完count的值后仍然等于3,这样就出现了错误。我们考虑的是只有两颗CPU的情况,但是现在市面上已经有8核手机了!如果8颗CPU同时工作的话,错误会更严重!

Atomic如何保证原子性?

要知道Atomic是如何保证原子性的,我们还得先来了解一下什么是CAS。CAS是一种有名的无锁算法(Compare and Swap)。他的核心思想是:当多个线程尝试使用CAS同时更新同一个变量时,只有一个线程能更新变量的值,而其他线程都失败,失败的线程并不会挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
还是我们上面的例子:
CPU2将count=3往内存写时,CAS中的3个操作数:V=3,A=2,B=3。当前内存中的count=3,CPU2的预期值应该是count=2,然而3不等于2,所以CPU2在这次竞争中失败。不再更新内存值。当线程2再次执行时,CPU2会从内存中获取最新的count值。这样就保证了线程安全。

这里写图片描述

Atomic正是引入了CAS无锁算法,我们来看一下AtomicInteger这个类:

private volatile int value;

AtomicInteger类内部使用一个被volatile修饰的int类型变量value来记录我们的值,为什么要使用volatile呢?

/**
     * Gets the current value.
     *
     * @return the current value
     */
    public final int get() {
        return value;
    }

AtomicInteger中有个get()方法,前面我们分析了,使用volatile关键字修饰的变量,在每次获取他的值的时候,我们都可以获取到他在内存中最新的值,既然有get()方法,那么肯定得有set()方法吧

 /**
     * Sets to the given value.
     *
     * @param newValue the new value
     */
    public final void set(int newValue) {
        value = newValue;
    }

set()方法也很简单,只是把一个新的值赋给了value,可是刚才我们分析了那么半天,他并没有保证原子操作啊!别着急,关键就在这里:

/**
     * Atomically sets to the given value and returns the old value.
     *
     * @param newValue the new value
     * @return the previous value
     */
    public final int getAndSet(int newValue) {
        for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
    }

是不是很熟悉!像不像刚才我们说的CAS算法,先是调用get()方法获取内存中最新的值,然后再调用compareAndSet方法来更新值,我们来看看compareAndSet方法

 /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return true if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

看见没!compareAndSwap不就是刚才我们讲的CAS算法吗!这样就能保证原子操作了!AtomicInteger还有很多方法,最终都是调用了compareAndSwap方法!
比如原子操作的++value

 /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

原子操作版的value++

/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }
如何正确的使用volatile关键字?

我们来看一个例子:

public class VolatileDemo {
    int x = 0 ;
    //注意:这里的b没有被volatile修饰
    boolean b = false;
    /**
     * 写操作
     */
    private void write(){
        x = 5;
        b = true;
    }
    /**
     * 读操作
     */
    private void read(){
        //如果b=false的话,就会无限循环,直到b=true才会执行结束,会打印出x的值
        while(!b){
        }
        System.out.println("x="+x);
    }
    public static void main(String[] args) throws Exception {
        final VolatileDemo volatileDemo = new VolatileDemo();
        //线程1执行写操作
        Thread thread1 = new Thread(new Runnable() {

            @Override
            public void run() {
                volatileDemo.write();
            }
        });
        //线程2执行读操作
        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                volatileDemo.read();
            }
        });
        //我们让线程2的读操作先执行
        thread2.start();
        //睡1毫秒,为了保证线程2比线程1先执行
        TimeUnit.MILLISECONDS.sleep(1);
        //再让线程1的写操作执行
        thread1.start();
        thread1.join();
        thread2.join();
        //等待线程1和线程2全部结束后,打印执行结束
        System.out.println("执行结束");
    }
}

注意我们的b没有用volatile修饰,我们先启动了线程2的读操作,后启动了线程1的写操作,由于线程1和线程2会保存x和b的副本到自己的工作内存中,线程2执行后,由于他副本b=false,所以会进入到无限循环中,线程1执行后修改的也是自己副本中的b=true,然而线程2无法立即察觉到,所以执行上面代码后,不会打印“执行结束”,因为线程2一直在执行!
这里写图片描述
运行之后会一直出于运行状态,并且没有打印“执行结束”
这里写图片描述

如果我们将b用volatile关键字修饰后,奇迹就出现了

//注意:这里的b被volatile修饰
    volatile boolean b = false;

此时的流程会是这样子
这里写图片描述
这里写图片描述
给b加了volatile关键字修饰后,线程1对b做了修改,然后会立即更新内存中的值,线程2通过嗅探发现自己的副本已经过期了,然后重新从内存中拿到b=true的值,然后跳出while循环,执行结束!

参考资料:

为什么volatile不能保证原子性而Atomic可以?
非阻塞同步算法与CAS(Compare and Swap)无锁算法
《Java并发编程艺术》

  • 10
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
### 回答1: volatileJava中的一个关键字,用于修饰变量。它的作用是告诉编译器,该变量可能会被多个线程同时访问,因此需要特殊处理,以保证线程安全。 具体来说,volatile关键字有以下几个特点: 1. 可见性:当一个线程修改了volatile变量的值,其他线程能够立即看到这个修改。 2. 有序性:volatile变量的读写操作会按照程序的顺序执行,不会被重排序。 3. 不保证原子性:虽然volatile变量能够保证可见性和有序性,但是它并不能保证多个线程同时修改变量时的原子性。 因此,如果需要保证原子性,需要使用synchronized关键字或者Lock接口来进行同步。 总之,volatile关键字Java中用于保证多线程访问变量的安全性的一种机制,它能够保证可见性和有序性,但是不能保证原子性。 ### 回答2: Java中的volatile关键字是一种轻量级的同步机制,用于确保多个线程之间的可见性和有序性。它可以用于修饰变量、类和方法。 1. 修饰变量:当一个变量被volatile修饰时,它会被立即写入到主内存中,并且每次读取变量时都会从主内存中重新获取最新的值。这样可以保证多个线程操作同一个变量时的可见性和一致性。 2. 修饰类:当一个类被volatile修饰时,它的实例变量就会被同步,而且每个线程都会获取最新的变量值。这样可以保证多线程操作同一对象时的可见性和一致性。 3. 修饰方法:当一个方法被volatile修饰时,它的调用会插入内存栅栏(memory barrier)指令,这可以保证方法调用前的修改操作都已经被写入主内存中,而方法调用后的读取操作也会重新从主内存中读取最新值。这样可以确保多线程之间的调用顺序和结果可见性。 需要注意的是,volatile并不能完全取代synchronized关键字,它只适用于并发度不高的场景,适用于只写入不读取的场景,不能保证复合操作的原子性。 总之,volatile关键字Java中具有广泛的应用,可以保证多线程之间的数据同步和可见性,但也需要谨慎使用,以免造成数据不一致和性能问题。 ### 回答3: Java中的volatile关键字意味着该变量在多个线程之间共享,并且每次访问该变量时都是最新的值。简单来说,volatile保证了线程之间的可见性和有序性。下面我们详细解释一下volatile的用法和作用。 1. 线程之间的可见性 volatile关键字保证了对该变量的读写操作对所有线程都是可见的。在没有用volatile关键字修饰变量的情况下,如果多个线程并发访问该变量,每个线程都会从自己的线程缓存中读取该变量的值,而不是直接从主存中读取。如果一个线程修改了该变量的值,但是其他线程不知道,那么可能导致其他线程获取到的数据不是最新的,从而引发一系列问题。而用了volatile关键字修饰该变量后,每次修改操作都会立即刷新到主存中,其他线程的缓存中的变量值也会被更新,从而保证了线程之间的可见性。 2. 线程之间的有序性 volatile关键字也保证了线程之间的有序性。多个线程并发访问同一个volatile变量时,JVM会保证每个线程按照程序指定的顺序执行操作。例如,在一个变量被volatile修饰的情况下,多个线程同时对该变量进行读写操作,JVM会保证先执行写操作的线程能够在后续的读操作中获取到最新的变量值。这么做的好处是,可以避免出现线程间操作顺序的乱序问题,从而保证了程序的正确性。 需要注意的是,并不是所有的变量都需要用volatile关键字修饰。只有在多个线程之间共享变量并且对变量的读写操作之间存在依赖关系的情况下,才需要使用volatile关键字。此外,volatile关键字不能保证原子性,如果需要保证操作的原子性,需要使用synchronized或者Lock等其他并发工具。 总之,volatile关键字Java中非常重要的关键字之一,它可以在多个线程之间保证可见性和有序性,从而保证了程序的正确性。在开发过程中,我们应该根据具体情况来选择是否使用volatile关键字,以及如何使用它。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值