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

为什么爱问volatile关键字

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

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

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

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

  • 保证了不同线程对该变量操作的内存可见性
  • 禁止指令重排序

基本上大家看过面试题都可以回答出这两点,点出了volatile关键字两大特性。针对这两大特性继续深入。

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

该问题涉及到Java内存模型(JVM)和它的内存可见性。

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

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

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

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

无论普通变量还是volatile变量都是如此,只不过volatile

变量保证新值能够立马同步到主内存,使用时也立即从主内存中刷新,保证了多线程操作时变量的可见性。而普通变量不能够保证。

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

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

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

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

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

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

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

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

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

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

public 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中的代码执行顺序可能会变成下面这样:

a = 1;              //1
flag = true; //2

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

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

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

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

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

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

如下代码,从直观上来讲,感觉输出结果为100,但实际上并不能保证,就是因为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(() -> {
for (int j = 0; j < 10; j++) {
test.increase();
}
}).start();
} //保证前面的进程都执行完
while (Thread.activeCount() > 2){
Thread.yield();
} System.out.println(test.inc); }
}

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

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

针对这种情况,只能使用synchronizedLock或并发包下的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时可拓展到JMMJava内存模型的区别,讲到原子性时可拓展到如何如何查看class字节码,讲到并发可拓展到线程并发。

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

面试中的volatile关键字的更多相关文章

  1. 面试官:volatile关键字用过吧?说一下作用和实现吧

    volatile    可见性的本质类似于CPU的缓存一致性问题,线程内部的副本类似于告诉缓存区 面试官:volatile关键字用过吧?说一下作用和实现吧 https://blog.csdn.net/ ...

  2. zz剖析为什么在多核多线程程序中要慎用volatile关键字?

    [摘要]编译器保证volatile自己的读写有序,但由于optimization和多线程可以和非volatile读写interleave,也就是不原子,也就是没有用.C++11 supposed会支持 ...

  3. java中的volatile关键字

    java中的volatile关键字 一个变量被声明为volatile类型,表示这个变量可能随时被其他线程改变,所以不能把它cache到线程内存(如寄存器)中. 一般情况下volatile不能代替syn ...

  4. 单例模式中的volatile关键字

    在之前学习了单例模式在多线程下的设计,疑惑为何要加volatile关键字.加与不加有什么区别呢?这里我们就来研究一下.单例模式的设计可以参考个人总结的这篇文章   背景:在早期的JVM中,synchr ...

  5. Java中的volatile关键字的功能

    Java中的volatile关键字的功能 volatile是java中的一个类型修饰符.它是被设计用来修饰被不同线程访问和修改的变量.如果不加入volatile,基本上会导致这样的结果:要么无法编写多 ...

  6. 深入理解Java中的volatile关键字

    在再有人问你Java内存模型是什么,就把这篇文章发给他中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性.可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized ...

  7. java中的Volatile关键字使用

    文章目录 什么时候使用volatile Happens-Before java中的Volatile关键字使用 在本文中,我们会介绍java中的一个关键字volatile. volatile的中文意思是 ...

  8. 面试时通过volatile关键字,全面展示线程内存模型的能力

    面试时,面试官经常会通过volatile关键字来考核候选人在多线程方面的能力,一旦被问题此类问题,大家可以通过如下的步骤全面这方面的能力.     1 首先通过内存模型说明volatile关键字的作用 ...

  9. C/C++中的volatile关键字

    volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据. 如果没有volatile关键字,则编译器可能优化读取和存 ...

随机推荐

  1. ubuntu 虚拟机复制后打开蓝屏解决办法

    sudo apt-get install xserver-xorg-lts-utopic sudo dpkg-reconfigure xserver-xorg-lts-utopic reboot

  2. python3(六) for while

    # Python的循环有两种,一种是for...in循环,依次把list或tuple中的每个元素迭代出来 names = ['Michael', 'Bob', 'Tracy'] for name in ...

  3. 在linux中使用mailx发送邮件

    [root@ml ~]# yum -y install mailx   #安装 [root@ml ~]# vim /etc/mail.rc 在最后一行添加(我这里使用的是qq邮箱): @qq.com ...

  4. intellJ svn控制错误

    电脑突然蓝屏了,重启打开intellj 后原本好好的项目是可以用intellj更新或者提交的,现在却都不能了,如图: 如上图:svn地址里是空白的,应该显示: 那到底是什么情况呢,就因为电脑崩溃了in ...

  5. 玩家的numpertpry 对象 中 不仅仅要同步 君主武将的等级,阶级也要同步

    因为好多列表 中 需要 批量查询 玩家的等级 和阶级(用来显示玩家icon颜色用的),如果阶级 在numperty 中已同步 的话,就不用批量去查玩家武将列表了.同理如果其他属性也经常用的话也可以同步 ...

  6. SwiftUI - 一步一步教你使用UIViewRepresentable封装网络加载视图(UIActivityIndicatorView)

    概述 网络加载视图,在一个联网的APP上可以讲得上是必须要的组件,在SwiftUI中它并没有提供如 UIKit 中的UIActivityIndicatorView直接提供给我们调用,但是我们可以通过 ...

  7. 在svg文间画图过程中放大缩小图片后,坐标偏移问题

    //鼠标坐标:在SVG经过缩放.偏移.ViewBox转换后,鼠标坐标值 var mouseCoord = { x : ., y : . }; //用户坐标:相对于原始SVG,坐标位置 var user ...

  8. Android应用架构分析

    一.res目录: 1.属性:Android必需: 2.作用:存放Android项目的各种资源文件.这些资源会自动生成R.java. 2.1.layout:存放界面布局文件. 2.2.strings.x ...

  9. 数据结构与算法--树(tree)结构

    树 二叉树 遍历原则:前序遍历是根左右, 中序遍历是左根右,后序遍历是左右根. 二叉搜索树 特点:对于树中的每个节点X,它的左子树中所有节点的值都小于X,右子树中所有节点的值都大于X. 遍历:采取二叉 ...

  10. 如果这篇文章说不清epoll的本质,那就过来掐死我吧!

    转载自:https://www.toutiao.com/i6683264188661367309/ 目录 一.从网卡接收数据说起 二.如何知道接收了数据? 三.进程阻塞为什么不占用cpu资源? 四.内 ...