Java面试官最爱问的volatile关键字

简介: Java面试官最爱问的volatile关键字

在Java的面试当中,面试官最爱问的就是volatile关键字相关的问题。经过多次面试之后,你是否思考过,为什么他们那么爱问volatile关键字相关的问题?而对于你,如果作为面试官,是否也会考虑采用volatile关键字作为切入点呢?


为什么爱问volatile关键字

爱问volatile关键字的面试官,大多数情况下都是有一定功底的,因为volatile作为切入点,往底层走可以切入Java内存模型(JMM),往并发方向走又可接切入Java并发编程,当然,再深入追究,JVM的底层操作、字节码的操作、单例都可以牵扯出来。


所以说懂的人提问题都是有门道的。那么,先整体来看看volatile关键字都设计到哪些点:内存可见性(JMM特性)、原子性(JMM特性)、禁止指令重排、线程并发、与synchronized的区别……再往深层次挖,可能就涉及到字节码、JVM等。


不过值得庆幸的是,如果你已经学习了微信公众号“程序新视界”JVM系列的文章,上面的知识点已经不是什么问题了,权当是复习了。那么,下面就以面试官提问的形式,在不看答案的情况下,尝试回答,看看学习效果如何。夺命连环问,开始……


面试官:说说volatile关键字的特性

被volatile修饰的共享变量,就具有了以下两点特性:


保证了不同线程对该变量操作的内存可见性;

禁止指令重排序;

回答的很好,点出了volatile关键字两大特性。针对该两大特性继续深入。


面试官:什么是内存可见性?能否举例说明?

该问题涉及到Java内存模型(JVM)和它的内存可见性特性,这里将前面系列《Java内存模型(JMM)详解》和《Java内存模型相关原则详解》中的部分内容整理出来回答。


先说内存模型:Java虚拟机规范试图定义一种Java内存模型(JMM),来屏蔽掉各种硬件和操作系统的内存访问差异,让Java程序在各种平台上都能达到一致的内存访问效果。


image.pngJava内存模型是通过变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值,将主内存作为传递媒介。可举例说明内存可见性的过程。

image.png本地内存A和B有主内存中共享变量x的副本,初始值都为0。线程A执行之后把x更新为1,存放在本地内存A中。当线程A和线程B需要通信时,线程A首先会把本地内存中x=1值刷新到主内存中,主内存中的x值变为1。随后,线程B到主内存中去读取更新后的x值,线程B的本地内存的x值也变为了1。


最后再说可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。


无论普通变量还是volatile变量都是如此,只不过volatile变量保证新值能够立马同步到主内存,使用时也立即从主内存刷新,保证了多线程操作时变量的可见性。而普通变量不能够保证。


面试官:提到JMM和可见性,能说说JMM的其他特性吗

我们知道JMM除了可见性,还有原子性和有序性。


原子性即一个操作或一系列是不可中断的。即使是在多个线程的情况下,操作一旦开始,就不会被其他线程干扰。


比如,对于一个静态变量int x两条线程同时对其赋值,线程A赋值为1,而线程B赋值为2,不管线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操作是没有干扰的,这就是原子性操作,不可被中断的。


在Java内存模型中有序性可归纳为这样一句话:如果在本线程内观察,所有操作都是有序的,如果在一个线程中观察另一个线程,所有操作都是无序的。


有序性是指对于单线程的执行代码,执行是按顺序依次进行的。但在多线程环境中,则可能出现乱序现象,因为在编译过程会出现“指令重排”,重排后的指令与原指令的顺序未必一致。


因此,上面归纳的前半句指的是线程内保证串行语义执行,后半句则指指“令重排现”象和“工作内存与主内存同步延迟”现象。


面试官:你多次提到指令重排,能举例说明吗?

CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化。但代码逻辑之间是存在一定的先后顺序,并发执行时按照不同的执行逻辑会得到不同的结果。


举个例说明多线程中可能出现的重排现象:


class ReOrderDemo {
    int a = 0;
    boolean flag = false;
    public void write() {
        a = 1;                   //1
        flag = true;             //2
    }
    public void read() {
        if (flag) {                //3
            int i =  a * a;        //4
            ……
        }
    }
}

在上面的代码中,单线程执行时,read方法能够获得flag的值进行判断,获得预期结果。但在多线程的情况下就可能出现不同的结果。比如,当线程A进行write操作时,由于指令重排,write方法中的代码执行顺序可能会变成下面这样:


flag = true;             //2

a = 1;                   //1

1

2

也就是说可能会先对flag赋值,然后再对a赋值。这在单线程中并不影响最终输出的结果。


但如果与此同时,B线程在调用read方法,那么就有可能出现flag为true但a还是0,这时进入第4步操作的结果就为0,而不是预期的1了。


而volatile关键词修饰的变量,会禁止指令重排的操作,从而在一定程度上避免了多线程中的问题。


面试官:volatile能保证原子性吗?

volatile保证了可见性和有序性(禁止指令重排),那么能否保证原子性呢?


volatile不能保证原子性,它只是对单个volatile变量的读/写具有原子性,但是对于类似i 这样的复合操作就无法保证了。


如下代码,从直观上来讲,感觉输出结果为10000,但实际上并不能保证,就是因为inc 操作属于复合操作。


public class Test {
    public volatile int inc = 0;
    public void increase() {
        inc  ;
    }
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i  ){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j  )
                        test.increase();
                };
            }.start();
        }
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }

假设线程A,读取了inc的值为10,然被阻塞,因未对变量进行修改,未触发volatile规则。线程B此时也读取inc的值,主存里inc的值依旧为10,做自增,然后立刻写回主存,值为11。此时线程A执行,由于工作内存里保存的是10,所以继续做自增,再写回主存,11又被写了一遍。所以虽然两个线程执行了两次increase(),结果却只加了一次。


有人说,volatile不是会使缓存行无效的吗?但是这里线程A读取之后并没有修改inc值,线程B读取时依旧是10。又有人说,线程B将11写回主存,不会把线程A的缓存行设为无效吗?只有在做读取操作时,发现自己缓存行无效,才会去读主存的值,而线程A的读取操作在线程B写入之前已经做过了,所以这里线程A只能继续做自增了。


针对这种情况,只能使用synchronized、Lock或并发包下的atomic的原子操作类。


面试官:刚提到synchronized,能说说它们之间的区别吗

volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别的;

volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性;

volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

面试官:还能举出其他例子说明volatile的作用吗

可举单例模式的实现,典型的双重检查锁定(DCL):


class Singleton{
    private volatile static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if(instance==null) { // 1
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton(); // 2
            }
        }
        return instance;
    }
}

这是一种懒汉的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排序,给instance加上了volatile。


为什么用了synchronized还要用volatile?具体来说就是synchronized虽然保证了原子性,但却没有保证指令重排序的正确性,会出现A线程执行初始化,但可能因为构造函数里面的操作太多了,所以A线程的instance实例还没有造出来,但已经被赋值了(即代码中2操作,先分配内存空间后构建对象)。


而B线程这时过来了(代码1操作,发现instance不为null),错以为instance已经被实例化出来,一用才发现instance尚未被初始化。要知道我们的线程虽然可以保证原子性,但程序可能是在多核CPU上执行。


小结

当然,针对volatile关键字还有其他方面的拓展,比如讲到JMM时可拓展到JMM与Java内存模型的区别,讲到原子性时可扩展到如何查看class字节码,讲到并发可扩展到线程并发的方法面面。


其实,不仅面试如此,在学习知识时也可以参考这种面试思维,多问几个为什么。将一个点,通过为什么拓展成一个知识网。


原文链接:《Java面试官最爱问的volatile关键字》


《面试官》系列文章:


《JVM之内存结构详解》

《面试官,不要再问我“Java GC垃圾回收机制”了》

《面试官,Java8 JVM内存结构变了,永久代到元空间》

《面试官,不要再问我“Java 垃圾收集器”了》

《Java虚拟机类加载器及双亲委派机制》

《Java内存模型(JMM)详解》

《Java内存模型相关原则详解》

《Java面试官最爱问的volatile关键字》



目录
相关文章
|
8月前
|
缓存 安全 Java
Volatile关键字与Java原子性的迷宫之旅
通过合理使用 `volatile`和原子操作,可以在提升程序性能的同时,确保程序的正确性和线程安全性。希望本文能帮助您更好地理解和应用这些并发编程中的关键概念。
186 21
|
6月前
|
存储 安全 Java
深入理解 Java 中的 instanceof 关键字
本文深入解析了 Java 中的 `instanceof` 关键字,探讨其在类型判断中的作用。作为二元操作符,`instanceof` 可用于检查对象是否为某类实例或实现特定接口,避免类型转换异常 (`ClassCastException`)。文章通过多态性下的类型判断、安全类型转换、接口实现检测及集合元素类型判定等实际应用场景,展示了 `instanceof` 的强大功能。掌握该关键字可提高代码健壮性,确保运行时类型安全。
388 0
|
7月前
|
Java C语言
课时8:Java程序基本概念(标识符与关键字)
课时8介绍Java程序中的标识符与关键字。标识符由字母、数字、下划线和美元符号组成,不能以数字开头且不能使用Java保留字。建议使用有意义的命名,如student_name、age。关键字是特殊标记,如蓝色字体所示。未使用的关键字有goto、const;特殊单词null、true、false不算关键字。JDK1.4后新增assert,JDK1.5后新增enum。
115 4
|
10月前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
258 5
Java 并发编程——volatile 关键字解析
|
9月前
|
Java 编译器 开发者
Java中的this关键字详解:深入理解与应用
本文深入解析了Java中`this`关键字的多种用法
1442 9
|
9月前
|
JSON Java 数据挖掘
利用 Java 代码获取淘宝关键字 API 接口
在数字化商业时代,精准把握市场动态与消费者需求是企业成功的关键。淘宝作为中国最大的电商平台之一,其海量数据中蕴含丰富的商业洞察。本文介绍如何通过Java代码高效、合规地获取淘宝关键字API接口数据,帮助商家优化产品布局、制定营销策略。主要内容包括: 1. **淘宝关键字API的价值**:洞察用户需求、优化产品标题与详情、制定营销策略。 2. **获取API接口的步骤**:注册账号、申请权限、搭建Java开发环境、编写调用代码、解析响应数据。 3. **注意事项**:遵守法律法规与平台规则,处理API调用限制。 通过这些步骤,商家可以在激烈的市场竞争中脱颖而出。
|
10月前
|
缓存 安全 Java
Java volatile关键字:你真的懂了吗?
`volatile` 是 Java 中的轻量级同步机制,主要用于保证多线程环境下共享变量的可见性和防止指令重排。它确保一个线程对 `volatile` 变量的修改能立即被其他线程看到,但不能保证原子性。典型应用场景包括状态标记、双重检查锁定和安全发布对象等。`volatile` 适用于布尔型、字节型等简单类型及引用类型,不适用于 `long` 和 `double` 类型。与 `synchronized` 不同,`volatile` 不提供互斥性,因此在需要互斥的场景下不能替代 `synchronized`。
3191 3
|
11月前
|
JavaScript 前端开发 Java
java中的this关键字
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。自学前端2年半,正向全栈进发。若我的文章对你有帮助,欢迎关注,持续更新中!🎉🎉🎉
159 9
|
11月前
|
设计模式 JavaScript 前端开发
java中的static关键字
欢迎来到瑞雨溪的博客,博主是一名热爱JavaScript和Vue的大一学生,致力于全栈开发。如果你从我的文章中受益,欢迎关注我,将持续分享更多优质内容。你的支持是我前进的动力!🎉🎉🎉
180 8
|
11月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
124 4