java并发之volatile关键字

Java面试中常常会涉及关于volatile的问题。本文梳理下volatile关键知识点。java

volatile字意为“易失性”,在Java中用作修饰对象变量。它不是Java特有,在C,C++,C#等编程语言也存在,只是在其它编程语言中使用有所差别,但整体语义一致。好比使用volatile 能阻止编译器对变量的读写优化。简单说,若是一个变量被修饰为volatile,至关于告诉系统说我容易变化,编译器你不要随便优化(重排序,缓存)我。git

Happens-before

规范上,Java内存模型遵行happens-beforegithub

volatile变量在多线程中,写线程和读线程具备happens-before关系。也就是写值的线程要在读取线程以前,而且读线程能彻底看见写线程的相关变量。面试

happens-before:若是两个有两个动做AB,A发生在B以前,那么A的顺序应该在B前面而且A的操做对B彻底可见。编程

happens-before 具备传递性,若是A发生在B以前,而B发生在C以前,那么A发生在C以前。缓存

如何保证可见性

多线程环境下counter变量的更新过程。线程1先从主存拷贝副本到CPU缓存,而后CPU执行counter=7,修改完后写入CPU缓存,等待时机同步到主存。在线程1同步主存前,线程2读到counter值依然为0。此时已经发生内存一致性错误(对于相同的共享数据,多线程读到视图不一致)。由于线程2看不见线程1操做结果,也将这个问题称为可见性问题安全

public class SharedObject {
    public int counter = 0;
}

由于多了缓存优化致使,致使可见性问题。因此volatile经过消除缓存(描述可能不太准确)来避免。例如当使用volatile修饰变量后,操做该变量读写直接与主存交互,跳过缓存层,保证其它读线程每次获取的都是最新值。多线程

public volatile int counter = 0;

volatile

volatile 不单只消除修饰的变量的缓存。事实上与之相关的变量在读写时也会消除缓存,如同使用了volatile同样。app

以下 years,months,days 三个变量中只有days是volatile,可是对years,months读写操做也和days时也会跳过缓存,其它线程每次读到的都是最新值。编程语言

public class MyClass {
    private int years;
    private int months
    private volatile int days;
    public int totalDays() {
        int total = this.days;
        total += months * 30;
        total += years * 365;
        return total;
    }
    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

这是为何?咱们分析一下。

一个写线程调用 update,读线程调用totalDays。单线程中,对于update方法,wa与wb存在happens-before关系, wawb 以前执行并对wb可见。

多线程中rc与wb存在happens-before关系,wbrc以前执行并对rc可见。根据 happens-before传递性,wa须要在rc前先执行并对rc可见。

由于wb是volatile变量,因此rc获取的years,months也是最新值。

happens-before

咱们知道出于性能缘由,JVM和CPU会对程序中的指令进行从新排序。若是update方法里面wawb顺序被重排,那它们的happens-before关系将不在成立。

happens-before

为了不这个问题,volatile对重排序作了保证 对于发生在volatile变量操做前的其余变量的操做不能从新排序

由此咱们获得volatile经过消除缓存防止重排保证线程的可见性。

volatile保证线程安全?

讨论线程安全,你们都会说起原子性顺序性可见性。volatile侧重于保证可见性,也就是当写的线程更新后,读线程总能得到最新值。在只有一个线程写,多个线程读的场景下,volatile能知足线程安全。可若是多个线程同时写入volatile变量时,则须要引入同步语义才能保证线程安全。

模拟10个线程同时写入volatile变量,一个线程读counter,执行完后正确结果应该是counter=10。

public static class WriterTask implements Runnable {
        private final ShareObject share;
        private final CountDownLatch countDownLatch;
        public WriterTask(ShareObject share, CountDownLatch countDownLatch) {
            this.share = share;
            this.countDownLatch = countDownLatch;
        }
        @Override
        public void run() {
            countDownLatch.countDown();
            share.increase();
        }
    }
    
    public class ShareObject {
        private volatile int counter;
        public void increase() {
            this.counter++;
        }
    }

执行结果出现counter=5或6 错误结果。

错误结果

错误结果

经过 synchronized,Lock或AtomicInteger 原子变量保证告终果的正确。

正确结果

完整demo https://gist.github.com/onlythinking/ba7ca7aa5faf00a58f4cedae474fa6f6

volatile性能

volatile变量带来可见性的保证,访问volatile变量还防止了指令重排序。不过这一切是以牺牲优化(消除缓存,直接操做主存开销增长)为代价,因此不该该滥用volatile,仅在确实须要加强变量可见性的时候使用。

总结

本文记录了volatile变量经过消除缓存,防止指令重排序来保证线程可见性,而且在多线程写入的变量的场景下,不保证线程安全。

欢迎你们留言交流,一块儿学习分享!!!

相关文章
相关标签/搜索