Java关键字——volatile底层原理分析

2 篇文章 0 订阅
1 篇文章 0 订阅

场景

volatile这个在多线程使用时能保证线程间的可见性。具体怎么用呢?举个例子:

public class VolatileVisibilityTest {
    private static  boolean initFlag = false;
    //private static volatile boolean initFlag = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println("waiting data");
            while (!initFlag) {

            }
            System.out.println("========success!!!");
        }).start();


        Thread.sleep(2000);

        new Thread(() -> prepareData()).start();


    }

    private static void prepareData() {
        System.out.println("开始修改initFlag...");
        initFlag = true;
        System.out.println("initFlag修改成功...");
    }
}

 

如上面的代码,结果是什么呢?会不会打印 “========success!!!”。我们来看结果

"C:\Program Files\Java\jdk1.8.0_212\bin\java.exe" ...waiting data开始修改initFlag...initFlag修改成功...

 

程序一直在whlie循环中,如果想要走出while循环,需要对initFlag 添加Volatile关键字修饰。​​​​​​​

"C:\Program Files\Java\jdk1.8.0_212\bin\java.exe" ...waiting data开始修改initFlag...initFlag修改成功...========success!!!

 

如上结果,加了Volatile后会打印 “========success!!!”。

 

JMM线程模型

 

如上图,线程获取数据是先从主内存中获取,然后将数据拷贝到工作内存。如果A线程改变了共享变量,此时只是在工作内存中改变了数据。对于其他线程来说是不可见的。

 

那共享变量加了关键字后为什么会可见了呢?

 

在解读上面图之前我们先来解释下几个指令操作。​​​​​​​

read(读取):从主内存读取数据load(载入):将主内存读取到的数据写入工作内存use(使用):从工作内存读取数据来计算assign(赋值):将计算好的值重新赋值到工作内存中store(存储):将工作内存数据写入主内存write(写入):将store过去的变量值赋值给主内存中的变量lock(锁定):将主内存变量加锁,标识为线程独占状态unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量

   

volatile保证可见性

如上图:线程1和线程2去主内存中拿(通过read指令)数据initFlag并将其放在(通过load指令)各自的工作内存中,此时initFlag的值为false。当线程2要修改initFlag的值为true,需要经历下面几步:

 

1)将工作内存中的initFlag 值加载(use指令)到线程2的执行引擎中

2)执行引擎将initFlag的值改为true

3)   将修改的值assign到工作内存中

4)将工作内存中的值store到主内存中

5)将主内存的变量赋值(通过write命令)

在第4步store时,数据会经过cpu总线,这时线程1会嗅探到值的变化。就会从主内存中获取新值。等等,这里可能你会有疑问,如果新值经过总线还没到达主内存中,这时线程1就去主内存中获取值,还是以前的旧值啊。那系统是怎么解决的呢。

当心智经过总线时,会上一把锁(lock),线程1是不能去主内存中获取值的,当执行了第5步,释放了锁(unlock),线程1才能去主内存中获取。

 

当线程1嗅探到值有变化,会让自己工作内存中的initFlag失效。然后线程1就执行下面步骤:

 

1)从主内存中read新值

2)将新值load进工作内存中

3)将工作内存中的变量赋新值

4)将新值use进线程1的执行引擎中

 

到这里变量initFlag就实现了可见性。

volatile不保证原子性

注意:volatile并不能保证原子性

public class VolatileAtomicTest {
    public static volatile int num = 0;

    public static void increase() {
        num++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increase();
                }
            });

            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println(num);
    }
}

 

上面的输出会是10000么,实际输出为:9985。为什么会这样呢

原因是虽然num在线程间是可见的,但是数据从线程的工作内存同步到主内存是需要时间的,这时其他线程自己有自己的计算,导致结果不可预测。num++经历了两步:a = num + 1;num = a。这两步不能保证原子性。如果想保证原子性,需要对increase()方法加锁。

  volatile禁止指令重排​​​​​​​

public class VolatileSerialTest {

    static int x = 0, y = 0;

    public static void main(String[] args) throws InterruptedException {

        Set<String> resultSet = new HashSet<>();

        Map<String, Integer> resultMap = new HashMap<>();

        for (int i = 0; i < 1000000; i++) {
            x = 0;
            y = 0;
            resultMap.clear();

            Thread one = new Thread(() -> {
                int a = y;
                x = 1;
                resultMap.put("a", a);
            });

            Thread other = new Thread(() -> {
                int b = x;
                y = 1;
                resultMap.put("b", b);
            });

            one.start();
            other.start();

            one.join();
            other.join();

            resultSet.add("a=" + resultMap.get("a") + "," + "b=" + resultMap.get("b"));
            System.out.println(resultSet);
        }
    }
}

 

上面代码你觉得会输出什么呢?

答案是:​​​​​​​

[a=0,b=1][a=1,b=0][a=1,b=1]   [a=0,b=0]   //不可思意吧

 

第四种输出不可思议吧,按照程序逻辑。是不可能出现[a=0,b=0]的情况的。其实CPU会对代码的执行顺序进行优化,及指令重排。

 

指令重排可以提高CPU处理速度。

 

 

如果给x,y加上volatile修饰,则不会出现指令重排,[a=0,b=0]就不会出现。

 

我们在编写懒汉式的单列模式时,也需要给对象加volatile修饰。

public class LazySimpleSingleton {
    private static volatile LazySimpleSingleton instance = null; //需要volatile修饰

    private LazySimpleSingleton(){
    }

    public static LazySimpleSingleton getInstance() {
        if (instance == null) {
            synchronized (LazySimpleSingleton.class) {
                if (instance == null) {
                    instance = new LazySimpleSingleton();
                }
            }
        }
        return instance;
    }
}

 

 

不加volatile,由于存在指令重排。线程A会出现b,c顺序颠倒情况。线程B进去获得实例可能未完全初始化。这时线程C执行方法getInstance(),返回的对象是未完全初始化的值。

往期推荐

扫码二维码,获取更多精彩。或微信搜Lvshen_9,可后台回复获取资料

1.回复"java" 获取java电子书;

2.回复"python"获取python电子书;

3.回复"算法"获取算法电子书;

4.回复"大数据"获取大数据电子书;

5.回复"spring"获取SpringBoot的学习视频。

6.回复"面试"获取一线大厂面试资料

7.回复"进阶之路"获取Java进阶之路的思维导图

8.回复"手册"获取阿里巴巴Java开发手册(嵩山终极版)

9.回复"总结"获取Java后端面试经验总结PDF版

10.回复"Redis"获取Redis命令手册,和Redis专项面试习题(PDF)

11.回复"并发导图"获取Java并发编程思维导图(xmind终极版)

另:点击【我的福利】有更多惊喜哦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值