彻底理解 volatile 关键字及应用场景,面试必问,小白都能看懂!

简介: 彻底理解 volatile 关键字及应用场景,面试必问,小白都能看懂!

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


Part1Java内存模型


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


所有变量储存在主内存。

每条线程拥有自己的工作内存,其中保存了主内存中线程使用到的变量的副本。

线程不能直接读写主内存中的变量,所有操作均在工作内存中完成。线程,主内存,工作内存的交互关系如图。


image.png


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


read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用


load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。


use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。


assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。


store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。


write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。


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


规则1:线程对变量执行的前一个动作是load时才能执行use,反之只有后一个动作是use时才能执行load。线程对变量的read,load,use动作关联,必须连续一起出现。-----这保证了线程每次使用变量时都需要从主存拿到最新的值,保证了其他线程修改的变量本线程能看到。


规则2:线程对变量执行的前一个动作是assign时才能执行store,反之只有后一个动作是store时才能执行assign。线程对变量的assign,store,write动作关联,必须连续一起出现。-----这保证了线程每次修改变量后都会立即同步回主内存,保证了本线程修改的变量其他线程能看到。


规则3:有线程T,变量V、变量W。假设动作A是T对V的use或assign动作,P是根据规则2、3与A关联的read或write动作;动作B是T对W的use或assign动作,Q是根据规则2、3与B关联的read或write动作。如果A先与B,那么P先与Q。------这保证了volatile修饰的变量不会被指令重排序优化,代码的执行顺序与程序的顺序相同。


Part2使用volatile关键字的特性


被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,可是多次运行后,每次的结果均小于预期值。显然上文的说法是有问题的。


2.png


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


1.png


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


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


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


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


运算结果依赖变量的当前值,并且有不止一个线程在修改变量的值。

变量需要与其他状态变量共同参与不变约束

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


模式1:状态标志

模式2:独立观察(independent observation)

模式3:“volatile bean” 模式

模式4:开销较低的“读-写锁”策略

具体场景 -- blog.csdn.net/vking_wang/article/details/9982709


禁止指令重排序优化。


由上文的规则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();可分解为:
memory =allocate(); //分配对象的内存空间
ctorInstance(memory); //初始化对象
instance =memory; //设置instance指向刚分配的内存地址


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


Part3总结

并发三特征可见性和有序性和原子性中,volatile


通过新值立即同步到主内存和每次使用前从主内存刷新机制保证了可见性。


通过禁止指令重排序保证了有序性。


无法保证原子性。


而我们知道,synchronized关键字


通过lock和unlock操作保证了原子性,


通过对一个变量unlock前,把变量同步回主内存中保证了可见性,


通过一个变量在同一时刻只允许一条线程对其进行lock操作保证了有序性。


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


相关文章
|
3月前
|
Java
【Java基础面试九】、说一说自动装箱、自动拆箱的应用场景
这篇文章介绍了Java中的自动装箱和自动拆箱概念:自动装箱允许将基本类型赋值给对应的包装类对象,而自动拆箱允许将包装类对象赋值给基本类型,从而简化了两者之间的转换过程。
【Java基础面试九】、说一说自动装箱、自动拆箱的应用场景
|
3月前
|
JavaScript
【Vue面试题十九】、Vue常用的修饰符有哪些有什么应用场景?
这篇文章详细介绍了Vue中的修饰符,包括表单修饰符、事件修饰符、鼠标按键修饰符、键值修饰符和`v-bind`修饰符,解释了它们各自的功能和应用场景,并通过代码示例展示了如何在实际开发中使用这些修饰符来简化事件处理和提高代码的可读性及效率。
【Vue面试题十九】、Vue常用的修饰符有哪些有什么应用场景?
|
3月前
|
缓存 安全 Java
面试官:说说volatile应用和实现原理?
面试官:说说volatile应用和实现原理?
44 1
|
3月前
|
缓存 Java
【多线程面试题二十三】、 说说你对读写锁的了解volatile关键字有什么用?
这篇文章讨论了Java中的`volatile`关键字,解释了它如何保证变量的可见性和禁止指令重排,以及它不能保证复合操作的原子性。
|
4月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
55 6
|
3月前
|
缓存 Java 编译器
一文搞懂volatile面试题
这篇文章是关于Java关键字volatile的详细介绍和分析,volatile是多线程访问共享变量时保证一致性的方案,性能优于synchronized,但不保证操作原子性,需要同步处理。
|
4月前
|
缓存 安全 Java
Java面试题:解释volatile关键字的作用,以及它如何保证内存的可见性
Java面试题:解释volatile关键字的作用,以及它如何保证内存的可见性
72 4
|
4月前
|
安全 Java
Java面试题:解释synchronized关键字在Java内存模型中的语义
Java面试题:解释synchronized关键字在Java内存模型中的语义
46 1
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
28天前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
59 2