面试中的volatile关键字
在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
内存模型是通过变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值,将主内存作为传递媒介。可以举例说明内存可见性的过程。
本地内存A
和B
有主内存中共享变量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
方法,那么就有可能出现flag
为true
但a
还是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
。又有人说,线程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
字节码,讲到并发可拓展到线程并发。
其实,不仅面试如此,在学习知识时也可以参考这种面试思维,多问几个为什么。将一个点,通过为什么展成面,这样就可以形成自己的知识网络。
面试中的volatile关键字的更多相关文章
- 面试官:volatile关键字用过吧?说一下作用和实现吧
volatile 可见性的本质类似于CPU的缓存一致性问题,线程内部的副本类似于告诉缓存区 面试官:volatile关键字用过吧?说一下作用和实现吧 https://blog.csdn.net/ ...
- zz剖析为什么在多核多线程程序中要慎用volatile关键字?
[摘要]编译器保证volatile自己的读写有序,但由于optimization和多线程可以和非volatile读写interleave,也就是不原子,也就是没有用.C++11 supposed会支持 ...
- java中的volatile关键字
java中的volatile关键字 一个变量被声明为volatile类型,表示这个变量可能随时被其他线程改变,所以不能把它cache到线程内存(如寄存器)中. 一般情况下volatile不能代替syn ...
- 单例模式中的volatile关键字
在之前学习了单例模式在多线程下的设计,疑惑为何要加volatile关键字.加与不加有什么区别呢?这里我们就来研究一下.单例模式的设计可以参考个人总结的这篇文章 背景:在早期的JVM中,synchr ...
- Java中的volatile关键字的功能
Java中的volatile关键字的功能 volatile是java中的一个类型修饰符.它是被设计用来修饰被不同线程访问和修改的变量.如果不加入volatile,基本上会导致这样的结果:要么无法编写多 ...
- 深入理解Java中的volatile关键字
在再有人问你Java内存模型是什么,就把这篇文章发给他中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性.可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized ...
- java中的Volatile关键字使用
文章目录 什么时候使用volatile Happens-Before java中的Volatile关键字使用 在本文中,我们会介绍java中的一个关键字volatile. volatile的中文意思是 ...
- 面试时通过volatile关键字,全面展示线程内存模型的能力
面试时,面试官经常会通过volatile关键字来考核候选人在多线程方面的能力,一旦被问题此类问题,大家可以通过如下的步骤全面这方面的能力. 1 首先通过内存模型说明volatile关键字的作用 ...
- C/C++中的volatile关键字
volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据. 如果没有volatile关键字,则编译器可能优化读取和存 ...
随机推荐
- Linux 权限管理篇(一)
可读 r 可写 w 可执行 x 档案属性: 第一栏:执行list -al后第一栏的十个标志[1 - 10] 1: d 目录 - 档案 l 连 ...
- ensp的基础路由命令,接口,下一跳的配置,入门必备
关于ensp入门事情,第一件事当是安装必备三件套:而后,应该是接触路由和PC机了,最烦人满屏代码,眼花缭乱: 今天写一篇零基础接触ensp的首次操作,PC-路由-路由-PC的互通实验: 实验要拉出两台 ...
- Django -->admin后台(后台管理可以直接往数据库添加数据)
一.使用pymysql时,必须加这两行(#如果使用mysql的数据库,请进行伪装 pymysql伪装为MySQLdb) import pymysqlpymysql.install_as_MySQLdb ...
- "字母全变小写"组件:<lowercase> —— 快应用组件库H-UI
 <import name="lowercase" src="../Common/ui/h-ui/text/c_text_lowercase">& ...
- day01,了解gcc
今天主要是学一下gcc 功能选项: 一. 1. gcc -E:表示预处理,把指令处理掉 2.gcc -o:改变目标文件名称 3. gcc -c: 表示只编译不链接(也就是不生成a.out) 4. g ...
- 解决click与hover(mouseover)的冲突问题
主要应用到一个知识点:$(selector).data('name','value'); <!DOCTYPE HTML> <html> <head> <met ...
- 面试问了解Linux内存管理吗?10张图给你安排的明明白白!
文章每周持续更新,各位的「三连」是对我最大的肯定.可以微信搜索公众号「 后端技术学堂 」第一时间阅读(一般比博客早更新一到两篇) 今天来带大家研究一下Linux内存管理.对于精通 CURD 的业务同学 ...
- D - Three Integers CodeForces - 1311D
题意: a<=b<=c 输出A,B,C要求B是A的倍数,C是B的倍数,并且输出a,b,c变成A,B,C需要的最小次数. 题解:写了半天的二分,后来发现思路错了,,,暴力就能过.. 三层fo ...
- SpringBoot全局异常处理与定制404页面
一.错误处理原理分析 使用SpringBoot创建的web项目中,当我们请求的页面不存在(http状态码为404),或者器发生异常(http状态码一般为500)时,SpringBoot就会给我们返回错 ...
- Charles抓包——弱网测试(客户端)
基础知识 网络延迟:网络延时指一个数据包从用户的计算机发送到网站服务器,然后再立即从网站服务器返回用户计算机的来回时间.通常使用网络管理工具PING(Packet Internet Grope)来测量 ...