前言

volatile相关的知识点,在面试过程中,属于基础问题,是必须要掌握的知识点,如果回答不上来会严重扣分的哦。

volatile关键字基本介绍

volatile可以看成是synchronized的一种轻量级的实现,但volatile并不能完全代替synchronized,volatile有synchronized可见性的特性,但没有synchronized原子性的特性。

可见性即用volatile关键字修饰的成员变量表明该变量不存在工作线程的副本,线程每次直接都从主内存中读取,每次读取的都是最新的值,这也就保证了变量对其他线程的可见性。

另外,使用volatile还能确保变量不能被重排序,保证了有序性。

  • 当一个变量定义为volatile之后,它将具备两种特性:

    • 保证此变量对所有线程的可见性
    • 禁止指令重排序优化
  • volatile与synchronized的区别:

    • 1、volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
    • 2、volatile保证数据的可见性,但是不保证原子性; 而synchronized是一种排他(互斥)的机制,既保证可见性,又保证原子性。
    • 3、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
    • 4、volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。

保证此变量对所有线程的可见性:

当一条线程修改了这个变量的值,新值对于其他线程可以说是可以立即得知的。Java内存模型规定了所有的变量都存储在主内存,每条线程还有自己的工作内存,线程的工作内存保存了该线程使用到的变量在主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读取主内存中的变量。

知识拓展:内存可见性

  • 概念:JVM内存模型:主内存 和 线程独立的 工作内存。Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存(比如CPU的寄存器),线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中。
  • 如何保证多个线程操作主内存的数据完整性是一个难题,Java内存模型也规定了工作内存与主内存之间交互的协议,定义了8种原子操作:
    • lock:将主内存中的变量锁定,为一个线程所独占。
    • unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量。
    • read:将主内存中的变量值读到工作内存当中。
    • load:将read读取的值保存到工作内存中的变量副本中。
    • use:将值传递给线程的代码执行引擎。
    • assign:将执行引擎处理返回的值重新赋值给变量副本。
    • store:将变量副本的值存储到主内存中。
    • write:将store存储的值写入到主内存的共享变量当中。

通过上面Java内存模型的概述,我们会注意到这么一个问题,每个线程在获取锁之后会在自己的工作内存来操作共享变量,操作完成之后将工作内存中的副本回写到主内存,并且在其它线程从主内存将变量同步回自己的工作内存之前,共享变量的改变对其是不可见的。

即其他线程的本地内存中的变量已经是过时的,并不是更新后的值。volatile保证可见性的原理是在每次访问变量时都会进行一次刷新,因此每次访问都是主内存中最新的版本。所以volatile关键字的作用之一就是保证变量修改的实时可见性。

即,volatile的特殊规则就是:

  • read、load、use动作必须连续出现。
  • assign、store、write动作必须连续出现。

所以,使用volatile变量能够保证:

  • 每次读取前必须先从主内存刷新最新的值。
  • 每次写入后必须立即同步回主内存当中。

也就是说,volatile关键字修饰的变量看到的是自己的最新值。线程1中对变量v的最新修改,对线程2是可见的。

禁止指令重排序优化:

volatile boolean isOK = false;

//假设以下代码在线程A执行
A.init();
isOK=true; //假设以下代码在线程B执行
while(!isOK){
sleep();
}
B.init();

A线程在初始化的时候,B线程处于睡眠状态,等待A线程完成初始化的时候才能够进行自己的初始化。这里的先后关系依赖于isOK这个变量。

如果没有volatile修饰isOK这个变量,那么isOK的赋值就可能出现在A.init()之前(指令重排序,Java虚拟机的一种优化措施),此时A没有初始化,而B的初始化就破坏了它们之前形成的那种依赖关系,可能就会出错。

知识拓展:指令重排序

  • 概念:指令重排序是JVM为了优化指令,提高程序运行效率,在不影响 单线程程序 执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。注意是单线程。多线程的情况下指令重排序就会给程序带来问题。

不同的指令间可能存在数据依赖。比如下面的语句:

  int l = 3; // (1)
int w = 4; // (2)
int s = l * w; // (3)

面积的计算依赖于l与w两个变量的赋值指令。而l与w无依赖关系。

重排序会遵守两个规则:

  • as-if-serial规则:as-if-serial规则是指不管如何重排序(编译器与处理器为了提高并行度),(单线程)程序的结果不能被改变。这是编译器、Runtime、处理器必须遵守的语义。
  • happens-before规则
    • 程序顺序规则:一个线程中的每个操作,happens-before于线程中的任意后续操作。
    • 监视器锁规则一个锁的解锁,happens-before于随后对这个锁的加锁。
    • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
    • 传递性:如果(A)happens-before(B),且(B)happens-before(C),那么(A)happens-before(C)。
    • 线程start()规则:主线程A启动线程B,线程B中可以看到主线程启动B之前的操作。也就是start() happens-before 线程B中的操作。
    • 线程join()规则:主线程A等待子线程B完成,当子线程B执行完毕后,主线程A可以看到线程B的所有操作。也就是说,子线程B中的任意操作,happens-before join()的返回。
    • 中断规则:一个线程调用另一个线程的interrupt,happens-before于被中断的线程发现中断。
    • 终结规则:一个对象的构造函数的结束,happens-before于这个对象finalizer的开始。
    • 概念:前一个操作的结果可以被后续的操作获取。讲直白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。
    • happens-before(先行发生)规则如下:

虽然,(1)-happensbefore ->(2),(2)-happens before->(3),但是计算顺序(1)(2)(3)与(2)(1)(3)对于l、w、area变量的结果并无区别。编译器、Runtime在优化时可以根据情况重排序(1)与(2),而丝毫不影响程序的结果。

  • volatile使用场景:
    • 1、对变量的写操作不依赖当前变量的值。
    • 2、该变量没有包含在其他变量的不变式中。
    • 如果正确使用volatile的话,必须依赖下以下种条件:

也可以这样理解,就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(i++)看上去类似一个单独操作,实际上它是一个由(读取-修改-写入)操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。

实现正确的操作需要使 i 的值在操作期间保持不变,而 volatile 变量无法实现这点。

  • 在以下两种情况下都必须使用volatile:
    • 1、状态的改变。
    • 2、读多写少的情况。

具体如下:

// 场景一:状态改变

/**
* 双重检查(DCL)
*/
public class Sun {
private static volatile Sun sunInstance; private Sun() {
} public static Sun getSunInstance() {
if (sunInstance == null) {
synchronized (Sun.class) {
if (sunInstance == null){
sunInstance = new Sun();
}
}
}
return sunInstance;
}
} // 场景二:读多写少 public class VolatileTest {
private volatile int value; //读操作,没有synchronized,提高性能
public int getValue() {
return value;
} //写操作,必须synchronized。因为x++不是原子操作
public synchronized int increment() {
return value++;
}
}

问题来了,volatile是如何防止指令重排序优化的呢?

答:

volatile关键字通过 “内存屏障” 的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。大多数的处理器都支持内存屏障的指令。

对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

知识拓展:内存屏障

内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。

内存屏障可以被分为以下几种类型:

  • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

面试官最爱的 volatile 关键字,这些问题你都搞懂了没?的更多相关文章

  1. 面试官最爱的volatile关键字

    在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模型(JMM),Java并发编程的一些特性 ...

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

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

  3. java面试官最爱问的垃圾回收机制,这位阿里P7大佬分析的属实到位

    前言 JVM 内存模型一共包括三个部分: 堆 ( Java代码可及的 Java堆 和 JVM自身使用的方法区). 栈 ( 服务Java方法的虚拟机栈 和 服务Native方法的本地方法栈 ) 保证程序 ...

  4. 关于http,测试面试官最爱问哪些?

    http和https的区别是什么? HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol),而 HTTPS 的全称是 Hypertext Transfer Prot ...

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

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

  6. 面试中的volatile关键字

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

  7. 大厂面试官竟然这么爱问Kafka,一连八个Kafka问题把我问蒙了?

    本文首发于公众号:五分钟学大数据 在面试的时候,发现很多面试官特别爱问Kafka相关的问题,这也不难理解,谁让Kafka是大数据领域中消息队列的唯一王者,单机十万级别的吞吐量,毫秒级别的延迟,这种天生 ...

  8. 围绕一个 volatile 关键字居然可以问出来 16 个问题

    对于 Java 每次面试就会想到多线程,多线程问题基本跑不了要问一下 volalite 关键字,可是我万万没想到居然一个 volatile 关键字可以连续问题出来 16 个问题!看下你能回答出来几个? ...

  9. 【Java并发编程】从CPU缓存模型到JMM来理解volatile关键字

    目录 并发编程三大特性 原子性 可见性 有序性 CPU缓存模型是什么 高速缓存为何出现? 缓存一致性问题 如何解决缓存不一致 JMM内存模型是什么 JMM的规定 Java对三大特性的保证 原子性 可见 ...

随机推荐

  1. 题解 洛谷 P4098 【[HEOI2013]ALO 】

    考虑原序列中的每一个值作为构成最终答案的那个次大值,那么其所在的合法区间最大时,其对答案的贡献最大. 一个值作为最大值时有两个合法的最大区间,一个是左边第二个比其大的位置和右边第一个比其大的位置构成的 ...

  2. Ubuntu Server 19.04配置静态IP

    这个/etc/netplan下默认有个文件50-cloud-init.yaml,直接修改它就行了 sudo vim /etc/netplan/50-cloud-init.yaml 网口名字enp0s3 ...

  3. Nginx 服务器配置支持SignalR (WebSocket)

    今天SignalR部署在测试环境服务器前端出现无法连接,前端报错如下: failed: Error during WebSocket handshake: Unexpected response co ...

  4. 手把手教你基于C#开发WinCC语音报警插件「附源代码」

    写在前面 众所周知,WinCC本身是可以利用C脚本或者VBS脚本来做语音报警,但是这种方式的本质是调用已存在的音频文件,想要实现实时播报报警信息是不行的,灵活性还不够,本文主要介绍基于C#/.NET开 ...

  5. vue中v-for

    在vue中我们只要操作数据,就可以渲染和更新数据,这背后的boss就是diff算法 vue和react的虚拟DOM的Diff算法大致相同,其核心是基于两个简单的假设: 1. 俩个相同组件产生类似DOM ...

  6. Qt-操作xml文件

    1  简介 参考视频:https://www.bilibili.com/video/BV1XW411x7AB?p=12 xml简介:可扩展标记语言,标准通用标记语言的子集,简称XML.是一种用于标记电 ...

  7. 21天学通C++(C++程序的组成部分)

    C++程序被组织成类,而类由成员函数和成员变量组成. 本章学习: 1)C++程序的组成部分. 2)各部分如何协同工作. 3)函数及其用途. 4)基本输入输出操作. C++程序划分为两个部分,以#大头的 ...

  8. 金字塔卷积:Pyramidal Convolution

    论文地址:https://arxiv.org/pdf/2006.11538.pdf github:https://github.com/iduta/pyconv 作者认为,当前CNN主要存在两个不足: ...

  9. 使用MacOS自带的SVN客户端

    原文链接:https://jingyan.baidu.com/article/5552ef479c1554518ffbc92f.html 摘要:mac环境下有自带的SVN服务端和客户端,SVN是许多公 ...

  10. PHP array_replace_recursive() 函数

    实例 递归地使用第二个数组($a2)的值替换第一个数组($a1)的值: <?php$a1=array("a"=>array("red")," ...