最后一面挂在volatile关键字上,面试官:重新学学Java吧!

为什么会有volatile关键字?

volatile: 易变的; 无定性的; 无常性的; 可能急剧波动的; 不稳定的; 易恶化的; 易挥发的; 易发散的;

从上面的单词本意我们可以知道这个关键词用于修饰那些易变的变量

为了让我们更好理解为什么volatile这个关键字的作用以及存在的意义

我们先来看一段代码:

package com.laoqin.juc;

/**
 * @Description TODO 测试volatile关键字
 * @author LaoQin
 */
public class TestVolatile {
    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();

        while (true){
            if(td.isFlag()){
                System.out.println("主线程获取到flag为true");
                break;
            }
        }
    }
}
class ThreadDemo implements Runnable{

    private boolean flag = false;

    @Override
    public void run() {
        /*睡眠2秒*/
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag="+ isFlag());
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

这段代码想表述的逻辑很简单,内部类实现了Runnable接口,run方法内部对flag进行了简单的赋值操作,并在主线程中写了一个死循环去不断判断flag的值,只要为true那么这个程序就会结束运行

但是事实也是如此吗?

我们等待了很久依然等不到程序结束,这是为什么呢?

我们得到的结果如下

flag=true

说明在线程内部我们的flag值确实是true,但是在主线程中的flag却一直是false,这就造成了我们的循环成为一个真正的”死循环”

这就涉及到一个“内存可见性”的问题了

内存可见性

JVM为了提升程序的运行效率,会为我们程序当中每一个线程分配一个独立的”缓存空间”,这个”缓存空间”对应着jvm调优参数中的 -Xss512k ,这表示为每个线程分配的”缓存空间”为512kb

程序运行过程中首先会有一个主存,拿上面的例子来说

主存中 flag = false;

然后启动两个线程,一个是读(主线程),一个是写(子线程)

因为子线程中休眠了2秒,所以是主线程先执行

因为子线程要改变数据,所以子线程是先把flag=false这条数据读到自己的”缓存”中来

然后在自己的内存空间先改变这个副本,然后再把这个值写回到主存中去

数据的运算都是在缓存中执行

主线程在子线程还没有改变主存值的时候就已经读取了false到自己的缓存

因为主线程调用的是while(true),JVM会调用系统底层代码,执行效率很高

甚至高到主线程没有机会再去主存中获取数据

这就是一个典型的内存可见性问题:

即两个线程在共享同一数据的时,共享数据的所有操作对于每个独立内存来说都是不可见的

对于以上的问题,我们可以通过同步锁来解决

package com.laoqin.juc;

/**
 * @Description TODO 测试volatile关键字
 * @author LaoQin
 */
public class TestVolatile {
    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();

        while (true){
            //改动了这里
            synchronized (td){
                if(td.isFlag()){
                    System.out.println("主线程获取到flag为true");
                    break;
                }
            }
        }
    }
}
class ThreadDemo implements Runnable{

    private boolean flag = false;

    @Override
    public void run() {
        /*睡眠2秒*/
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag="+ isFlag());
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

这样通过给对象加锁,就可以解决这个问题,即保持多个线程之间数据的同步(锁住共享数据或者共享数据所在对象)

得到以下效果

主线程获取到flag为true
flag=true

但是加锁意味着我们程序的效率将会变得极其低下

当有多个线程同时访问的时候,后来的线程必须要等待前面的线程释放被锁资源才能进行操作

这就是volatile存在的意义

volatile用法

volatile能保证多个线程在操作同一个数据时,这个数据对于所有线程来说是可见的

底层是因为volatile会让jvm去调用计算机底层的”内存栅栏”

内存屏障,也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。

语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。

我们可以简单理解为volatile能让所有线程的操作都在主存中完成,这样就规避了内存可见性的问题

这样会比锁的效率高很多,但是还是会比不加该关键字运行效率低不少

原因是JVM底层优化逻辑中对violatile修饰的变量会进行重排序,这个会比较耗时

以下是使用volatile的代码

package com.laoqin.juc;

/**
 * @Description TODO 测试volatile关键字
 * @author LaoQin
 */
public class TestVolatile {
    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();

        while (true){
                if(td.isFlag()){
                    System.out.println("主线程获取到flag为true");
                    break;
            }
        }
    }
}
class ThreadDemo implements Runnable{

    //改动了这里
    private volatile boolean flag = false;

    @Override
    public void run() {
        /*睡眠2秒*/
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag="+ isFlag());
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

得到的结果和加锁的一样

主线程获取到flag为true
flag=true

volatile和synchronized异同

相较于synchronized,volatile是一种轻量级的”数据同步策略”

但是volatile不具备”互斥性”,即synchronized修饰的数据一旦上锁后别的线程是无法操作该数据的,但volatile修饰的变量只是会让该数据在主存中完成操作,并不会让数据具有”互斥性”

同时volatile也不能保证变量的”原子性”,即volatile不能保证变量是一个不可分割的整体

相信通过这篇文章简短的叙述,各位也对volatile有了一定基础的认识,更多高级的技巧方丈建议看官可以取阅读一下JDK源码,看看Oracle Java小组的大神们都是如何将这个关键字用得出神入化的!

方丈全栈©版权所有,转载请注明出处,如有盗用,后果自负!

版权声明:本文为fz-fullstack原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/fz-fullstack/p/13125299.html