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. Linux 权限管理篇(一)

    可读        r 可写        w 可执行        x 档案属性: 第一栏:执行list -al后第一栏的十个标志[1 - 10] 1: d    目录 -    档案 l    连 ...

  2. ensp的基础路由命令,接口,下一跳的配置,入门必备

    关于ensp入门事情,第一件事当是安装必备三件套:而后,应该是接触路由和PC机了,最烦人满屏代码,眼花缭乱: 今天写一篇零基础接触ensp的首次操作,PC-路由-路由-PC的互通实验: 实验要拉出两台 ...

  3. Django -->admin后台(后台管理可以直接往数据库添加数据)

    一.使用pymysql时,必须加这两行(#如果使用mysql的数据库,请进行伪装 pymysql伪装为MySQLdb) import pymysqlpymysql.install_as_MySQLdb ...

  4. "字母全变小写"组件:<lowercase> —— 快应用组件库H-UI

     <import name="lowercase" src="../Common/ui/h-ui/text/c_text_lowercase">& ...

  5. day01,了解gcc

    今天主要是学一下gcc 功能选项: 一. 1.  gcc -E:表示预处理,把指令处理掉 2.gcc -o:改变目标文件名称 3. gcc -c: 表示只编译不链接(也就是不生成a.out) 4. g ...

  6. 解决click与hover(mouseover)的冲突问题

    主要应用到一个知识点:$(selector).data('name','value'); <!DOCTYPE HTML> <html> <head> <met ...

  7. 面试问了解Linux内存管理吗?10张图给你安排的明明白白!

    文章每周持续更新,各位的「三连」是对我最大的肯定.可以微信搜索公众号「 后端技术学堂 」第一时间阅读(一般比博客早更新一到两篇) 今天来带大家研究一下Linux内存管理.对于精通 CURD 的业务同学 ...

  8. D - Three Integers CodeForces - 1311D

    题意: a<=b<=c 输出A,B,C要求B是A的倍数,C是B的倍数,并且输出a,b,c变成A,B,C需要的最小次数. 题解:写了半天的二分,后来发现思路错了,,,暴力就能过.. 三层fo ...

  9. SpringBoot全局异常处理与定制404页面

    一.错误处理原理分析 使用SpringBoot创建的web项目中,当我们请求的页面不存在(http状态码为404),或者器发生异常(http状态码一般为500)时,SpringBoot就会给我们返回错 ...

  10. Charles抓包——弱网测试(客户端)

    基础知识 网络延迟:网络延时指一个数据包从用户的计算机发送到网站服务器,然后再立即从网站服务器返回用户计算机的来回时间.通常使用网络管理工具PING(Packet Internet Grope)来测量 ...