面试中的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关键字,则编译器可能优化读取和存 ...
随机推荐
- ubuntu 虚拟机复制后打开蓝屏解决办法
sudo apt-get install xserver-xorg-lts-utopic sudo dpkg-reconfigure xserver-xorg-lts-utopic reboot
- python3(六) for while
# Python的循环有两种,一种是for...in循环,依次把list或tuple中的每个元素迭代出来 names = ['Michael', 'Bob', 'Tracy'] for name in ...
- 在linux中使用mailx发送邮件
[root@ml ~]# yum -y install mailx #安装 [root@ml ~]# vim /etc/mail.rc 在最后一行添加(我这里使用的是qq邮箱): @qq.com ...
- intellJ svn控制错误
电脑突然蓝屏了,重启打开intellj 后原本好好的项目是可以用intellj更新或者提交的,现在却都不能了,如图: 如上图:svn地址里是空白的,应该显示: 那到底是什么情况呢,就因为电脑崩溃了in ...
- 玩家的numpertpry 对象 中 不仅仅要同步 君主武将的等级,阶级也要同步
因为好多列表 中 需要 批量查询 玩家的等级 和阶级(用来显示玩家icon颜色用的),如果阶级 在numperty 中已同步 的话,就不用批量去查玩家武将列表了.同理如果其他属性也经常用的话也可以同步 ...
- SwiftUI - 一步一步教你使用UIViewRepresentable封装网络加载视图(UIActivityIndicatorView)
概述 网络加载视图,在一个联网的APP上可以讲得上是必须要的组件,在SwiftUI中它并没有提供如 UIKit 中的UIActivityIndicatorView直接提供给我们调用,但是我们可以通过 ...
- 在svg文间画图过程中放大缩小图片后,坐标偏移问题
//鼠标坐标:在SVG经过缩放.偏移.ViewBox转换后,鼠标坐标值 var mouseCoord = { x : ., y : . }; //用户坐标:相对于原始SVG,坐标位置 var user ...
- Android应用架构分析
一.res目录: 1.属性:Android必需: 2.作用:存放Android项目的各种资源文件.这些资源会自动生成R.java. 2.1.layout:存放界面布局文件. 2.2.strings.x ...
- 数据结构与算法--树(tree)结构
树 二叉树 遍历原则:前序遍历是根左右, 中序遍历是左根右,后序遍历是左右根. 二叉搜索树 特点:对于树中的每个节点X,它的左子树中所有节点的值都小于X,右子树中所有节点的值都大于X. 遍历:采取二叉 ...
- 如果这篇文章说不清epoll的本质,那就过来掐死我吧!
转载自:https://www.toutiao.com/i6683264188661367309/ 目录 一.从网卡接收数据说起 二.如何知道接收了数据? 三.进程阻塞为什么不占用cpu资源? 四.内 ...