其他分享
首页 > 其他分享> > 面试必问的 volatile 关键字,通俗易懂,看完还不懂你打我!

面试必问的 volatile 关键字,通俗易懂,看完还不懂你打我!

作者:互联网

来源:blog.csdn.net/fumitzuki/article/details/81630048

volatile关键字是由JVM提供的最轻量级同步机制。与被滥用的synchronized不同,我们并不习惯使用它。想要正确且完全的理解它并不容易。

Java内存模型

Java内存模型由Java虚拟机规范定义,用来屏蔽各个平台的硬件差异。简单来说:

线程,主内存,工作内存的交互关系如图。

内存间的交互操作有很多,和volatile有关的操作为:

对被volatile修饰的变量进行操作时,需要满足以下规则:

使用volatile关键字的特性

1.被volatile修饰的变量保证对所有线程可见。

由上文的规则1、2可知,volatile变量对所有线程是立即可见的,在各个线程中不存在一致性问题。那么,我们是否能得出结论:volatile变量在并发运算下是线程安全的呢?

这确实是一个非常常见的误解,写个简单的例子:

public class VolatileTest extends Thread{
    static volatile int increase = 0;
    static AtomicInteger aInteger=new AtomicInteger();//对照组
    static void increaseFun() {
        increase++;
        aInteger.incrementAndGet();
    }
    public void run(){
        int i=0;
        while (i < 10000) {
            increaseFun();
            i++;
        }
    }
    public static void main(String[] args) {
        VolatileTest vt = new VolatileTest();
        int THREAD_NUM = 10;
        Thread[] threads = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {
            threads[i] = new Thread(vt, "线程" + i);
            threads[i].start();
        }
        //idea中会返回主线程和守护线程,如果用Eclipse的话改为1
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("volatile的值: "+increase);
        System.out.println("AtomicInteger的值: "+aInteger);
    }
}

这个程序我们跑了10个线程同时对volatile修饰的变量进行10000的自增操作(AtomicInteger实现了原子性,作为对照组),如果volatile变量是并发安全的话,运行结果应该为100000,可是多次运行后,每次的结果均小于预期值。显然上文的说法是有问题的。

volatile修饰的变量并不保值原子性,所以在上述的例子中,用volatile来保证线程安全不靠谱。我们用Javap对这段代码进行反编译,为什么不靠谱简直一目了然:

getstatic指令把increase的值拿到了操作栈的顶部,此时由于volatile的规则,该值是正确的。

iconst_1和iadd指令在执行的时候increase的值很有可能已经被其他线程加大,此时栈顶的值过期。

putstatic指令接着把过期的值同步回主存,导致了最终结果较小。

volatile关键字只保证可见性,所以在以下情况中,需要使用锁来保证原子性:

那么volatile的这个特性的使用场景是什么呢?

具体场景:

https://blog.csdn.net/vking_wang/article/details/9982709

2.禁止指令重排序优化。

由上文的规则3可知,volatile变量的第二个语义是禁止指令重排序。指令重排序是什么?简单点说就是

jvm会把代码中没有依赖赋值的地方打乱执行顺序,由于一些规则限定,我们在单线程内观察不到打乱的现象(线程内表现为串行的语义),但是在并发程序中,从别的线程看另一个线程,操作是无序的。

一个非常经典的指令重排序例子:

public class SingletonTest {
    private volatile static SingletonTest instance = null;
    private SingletonTest() { }
    public static SingletonTest getInstance() {
        if(instance == null) {
            synchronized (SingletonTest.class){
                if(instance == null) {
                    instance = new SingletonTest();  //非原子操作
                }
            }
        }
        return instance;
    }
}

这是单例模式中的“双重检查加锁模式”,我们看到instance用了volatile修饰,由于 instance = new SingletonTest();可分解为:

  1. memory =allocate(); //分配对象的内存空间
  2. ctorInstance(memory); //初始化对象
  3. instance =memory; //设置instance指向刚分配的内存地址

操作2依赖1,但是操作3不依赖2,所以有可能出现1,3,2的顺序,当出现这种顺序的时候,虽然instance不为空,但是对象也有可能没有正确初始化,会出错。

总结

并发三特征可见性和有序性和原子性中,volatile通过新值立即同步到主内存和每次使用前从主内存刷新机制保证了可见性。通过禁止指令重排序保证了有序性。无法保证原子性。

而我们知道,synchronized关键字通过lock和unlock操作保证了原子性,通过对一个变量unlock前,把变量同步回主内存中保证了可见性,通过一个变量在同一时刻只允许一条线程对其进行lock操作保证了有序性。

他的“万能”也间接导致了我们对synchronized关键字的滥用,越泛用的控制,对性能的影响也越大,虽然jvm不断的对synchronized关键字进行各种各样的优化,但是我们还是要在合适的时候想起volatile关键字啊,哈哈哈哈。

近期热文推荐:

1.1,000+ 道 Java面试题及答案整理(2021最新版)

2.别在再满屏的 if/ else 了,试试策略模式,真香!!

3.卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.5 重磅发布,黑暗模式太炸了!

5.《Java开发手册(嵩山版)》最新发布,速速下载!

觉得不错,别忘了随手点赞+转发哦!

标签:必问,变量,通俗易懂,instance,线程,内存,操作,volatile
来源: https://www.cnblogs.com/javastack/p/15499399.html