面霸的自我修养:volatile专题
王有志,一个分享硬核Java技术的互金摸鱼侠
加入Java人的提桶跑路群:共同富裕的Java人
今天是《面霸的自我修养》第4篇文章,我们一起来看看面试中会问到哪些关于volatile的问题吧。
数据来源:
- 大部分来自于各机构(Java之父,Java继父,某灵,某泡,某客)以及各博主整理文档;
- 小部分来自于我以及身边朋友的实际经理,题目上会做出标识,并注明面试公司。
叠“BUFF”:
- 八股文通常出现在面试的第一二轮,是“敲门砖”,但仅仅掌握八股文并不能帮助你拿下Offer;
- 由于本人水平有限,文中难免出现错误,还请大家以批评指正为主,尽量不要喷~~
- 本文及历史文章已经完成PDF文档的制作,提取关键字【面霸的自我修养】。
理论篇
指令重排
难易程度:
重要程度:
面试公司:无
指令重排是一种优化技术,通过指令乱序执行(Out Of Order Execution,简称OoOE或OOE)提高处理器的执行效率和性能。
以下内容摘自维基百科:
在计算机工程领域,乱序执行(错序执行,英语:out-of-order execution,简称OoOE或OOE)是一种应用在高性能微处理器中来利用指令周期以避免特定类型的延迟消耗的范式。在这种范式中,处理器根据输入数据的可用性确定执行指令的顺序,而不是根据程序的原始数据决定。在这种方式下,可以避免因为获取下一条程序指令所引起的处理器等待,取而代之的处理下一条可以立即执行的指令。
指令重排的基础建立在保证当线程环境下语义准确性的前提下,而不能保证多线程环境下的语义。
内存屏障
难易程度:
重要程度:
面试公司:无
内存屏障(Memory barrier),也称内存栅栏,内存栅障,屏障指令等,是一类同步指令,它使CPU或编译器进行操作时严格按照一定的顺序执行,即保证内存屏障前后的指令不会因为指令重排而导致乱序执行。
JVM中定义了7种屏障:
class OrderAccess : private Atomic {
public:
static void loadload();
static void storestore();
static void loadstore();
static void storeload();
static void acquire();
static void release();
static void fence();
}
其中最重要的是4种基本的内存屏障:
- LoadLoad,指令:
Load1; LoadLoad; Load2。确保Load1在Load2及之后的读操作前完成读操作,Load1前的Load指令不能重排序到Load2及之后的读操作后; - StoreStore,指令:
Store1; StoreStore; Store2。确保Store1在Store2及之后的写操作前完成写操作,且Stroe1写操作的结果对Store2可见,Store1前的Store指令不能重排序到Store2及之后的写操作后; - LoadStore,指令:
Load1; LoadStore; Store2。确保Load1在Store2及之后的写操作前完成读操作,Load1前的Load指令不能重排序到Store2及之后的写操作后; - StoreLoad:指令:
Store1; StoreLoad; Load2。确保Store1在Load2及之后的Load指令前完成写操作,Store1前的Store指令不能重排序到Load2及之后的Load指令后。
至于acquire,release和fence,我们通过一张表格来表示它们与4种基本内存屏障的对应关系:

Tips:
- 内存屏障的定义位于orderAccess.hpp中,强烈建议阅读注释中的“Memory Access Ordering Model”;
- 重点理解4种基本内存屏障实现的功能即可,JVM源码对的部分了解即可,也别往下卷了,啥时候是个头啊;
- Java中定义的内存屏障屏蔽不同操作系统间内存屏障的差异,使得不同的操作系统表现出一致的语义。
原理篇
volatile是什么?
难易程度:
重要程度:
面试公司:腾讯
volatile是Java提供的关键字,可以用来修饰成员变量。volatile提供了两个能力:
- 保证被修饰变量在多线程环境下的可见性
- 禁止被修饰变量的指令重排
volatile保证可见性的例子:
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
}
System.out.println("线程:" + Thread.currentThread().getName() + ",flag状态:" + flag);
}, "block_thread").start();
TimeUnit.MICROSECONDS.sleep(500);
new Thread(() -> {
flag = false;
System.out.println("线程:" + Thread.currentThread().getName() + ",flag状态:" + flag);
}, "change_thread").start();
}
删除修饰flag的volatile后,block_thread无法“察觉”到change_thread对flag的修改,因此会“沉迷”wile循环无法自拔。
**volatile**禁止指令重排的例子:
经典的双检锁单例模式,在下一题中解释不使用volatile带来的有序性问题。
public static class Singleton {
private volatile Singleton instance;
public Singleton getInstance() {
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile的实现原理
难易程度:
重要程度:
面试公司:百度,OPPO,丰巢,美团,乐信
被volatile修饰的变量在生成字节码时会被标记上ACC_VOLATILE,当JVM读取到该标记时会按照JMM中定义的volatile语义处理。
以经典的双检锁单例模式为例:
public class Singleton {
static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
编译后的部分字节码如下:
public class com.wyz.keyword.keyword_volatile.Singleton
static volatile com.wyz.keyword.keyword_volatile.Singleton instance;
flags:(0x0048) ACC_STATIC, ACC_VOLATILE
public static com.wyz.keyword.keyword_volatile.Singleton getInstance();
Code:
stack=2, locals=2, args_size=0
24: putstatic #7 // Field instance:Lcom/wyz/keyword/keyword_volatile/Singleton;
37: getstatic #7 // Field instance:Lcom/wyz/keyword/keyword_volatile/Singleton;
字节码中第7行和第8行的两个指令:putstatic和getstatic(非静态变量对应putfield和gettfield)用于操作静态变量instance,这两条指令的源码位于bytecodeInterpreter中,以下仅截取关键部分源码。
volatile变量的写操作
putstatic和putfield指令:
CASE(_putfield):
CASE(_putstatic):
{
if ((Bytecodes::Code)opcode == Bytecodes::_putstatic) {
// static的处理方式
} else {
// 非static的处理方式
}
// ACC_VOLATILE -> JVM_ACC_VOLATILE -> is_volatile()
if (cache->is_volatile()) {
// volatile变量的处理方式
if (tos_type == itos) {
obj->release_int_field_put(field_offset, STACK_INT(-1));
}else {
// 省略了超多的类型判断
}
OrderAccess::storeload();
} else {
// 非volatile变量的处理方式
}
}
JVM在处理完volatile类型变量的写操作后,加入OrderAccess::storeload,保证volatile变量的写操作对所有后续的读操作可见。
volatile变量的读操作
getstatic和gettfield指令:
CASE(_getfield):
CASE(_getstatic):
oop obj;
if ((Bytecodes::Code)opcode == Bytecodes::_getstatic) {
// static变量的处理
} else {
// 非static变量的处理
}
if (cache->is_volatile()) {
// volatile变量的处理方式
//
if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
OrderAccess::fence();
}
if (tos_type == atos) {
VERIFY_OOP(obj->obj_field_acquire(field_offset));
SET_STACK_OBJECT(obj->obj_field_acquire(field_offset), -1);
} else {
// 省略了超多的类型判断
}
} else {
// 非volatile变量的处理
}
JVM在处理volatile变量的读操作前,加入OrderAccess::fence,保证了volatile变量的读操作前所有对volatile变量的写操作已经对其它处理器可见。
是否使用OrderAccess::fence,由常量support_IRIW_for_not_multiple_copy_atomic_cpu决定,该常量定义在globalDefinitions.hpp文件中:
#ifdef CPU_NOT_MULTIPLE_COPY_ATOMIC
const bool support_IRIW_for_not_multiple_copy_atomic_cpu = true;
#else
const bool support_IRIW_for_not_multiple_copy_atomic_cpu = false;
#endif
该常量指的是支持IRIW但不支持Mutiple Copy Atomic(MCA模型,Multi-copy Atomicity)的CPU,在这类CPU中volatile变量的getstatic和gettfield指令需要使用OrderAccess::fence来保证语义的正确性,否则不需要使用。
Tips:文末参考资料中提供了关于IRIW和MCA模型的部分资料,感兴趣的可以自行阅读。
synchronized和volatile有哪些区别?
难易程度:
重要程度:
面试公司:无
synchronized和volatile都是Java中的关键字,但它们能够修饰的范围不同:
synchronized用来修饰方法和代码块;volatile用来修饰变量。
另外它们的作用也并不是完全相同:
synchronized对可见性,有序性和原子性都做出了保证;volatile保证了被修饰变量的可见性,禁止被修饰变量的指令重排。
举个指令重排的例子:
int a, b, c, d;
int count;
public static void main(String[] args) {
a += 1;
b += 1;
count += 1;
c += 1;
d += 1;
}
这段代码中,可能发生的顺序是:
当我们使用volatile修饰count后,count += 1;一定是发生在a += 1;和b += 1;之后,发生在c += 1;和d += 1;之前的。也就是说,即便不存在数据依赖,对变量a,b,c或d的操作也不能与对变量count的操作发生指令重排。
至于a += 1;,b += 1;和c += 1;,d += 1;之间的指令重排,被volatile修饰的count并不关心。
d += 1;
c += 1;
b += 1;
a += 1;
count += 1;
Tips:synchronized与volatile在保证有序性上的原理是不同的。synchronized限制了同一时间只有一个线程可以执行被修饰的代码,因此能够保证有序性(虽然指令可能发生了重排序);volatile则是禁止了指令重排,来保证程序的有序性。
使用volatile变量就一定是并发安全的吗?
难易程度:
重要程度:
面试公司:美团
并不是的,并发编程中有3个问题:
- 可见性问题
- 有序性问题
- 原子性问题
volatile关键字通过JVM实现的内存屏障保证了可见性和有序性,但没有对运算操作原子性做出任何保证。
比如最常见的自增操作的例子:
private volatile static int count = 0;
public static void main(String[] args) {
new Thread(() -> {
for(int i = 0; i < 300000; i++) {
count++;
System.out.println("T1:" + count);
}
}).start();
new Thread(() -> {
for(int i = 0; i < 300000; i++) {
count++;
System.out.println("T2:" + count);
}
}).start();
}
执行上面的程序,最后的结果可能并不是预期的600000,而是一个小于600000的数字(如果电脑的CPU非常“屌”,可以试试调大循环的数字来复现这个问题),这是因为count++操作包含了3个动作,而这3个动作并不是原子性执行的:
- 读取变量count
- count进行自增操作
- 将count写入工作内存
以上的操作可能被分开执行,导致出现如下情况:

简单解释下第7步操作,线程T1重新开始执行,发现缓存已经失效,此时线程T1重新读取内存中的数据,但由于T1已经执行过自增操作,因此不会重新执行自增操作,所以此时写入内存的仍然是线程T1阻塞前计算的结果。
Tips:
- 以上内容需要大家熟悉缓存一致性协议MESI的基本内容;
- MESI是缓存一致性协议的一种,但缓存一致性协议并不仅仅是MESI,常见的还有MOSI协议,MOESI协议等。
参考资料
- 乱序执行(维基百科)
- 内存屏障(维基百科)
- MESI协议(维基百科)
- MESI协议(百度百科)
- 硬核的volatile考点分析
- Relaxed memory models must be rigorous
- Multi-copy Atomicity and Barriers
如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠王有志,我们下次再见!
面霸的自我修养:volatile专题的更多相关文章
- GIS制图人员的自我修养(1)--制图误区
GIS制图人员的自我修养 by 李远祥 最近一直坚持写GIS制图的技术专题,并不是为了要介绍有什么好的技术和方法去制图,而是要告诉所有从事这一方向的人员一个铁铮铮的实现--要做好GIS制图,必须加强自 ...
- IT技术管理者的自我修养
1. 前言 本来写<IT技术管理者的自我修养>与<IT技术人员的自我修养>是一开始就有的想法.但发表<IT技术人员的自我修养>后,收到了不少良好的反馈,博客园的编辑 ...
- 《web全栈工程师的自我修养》读书笔记
有幸读了yuguo<web全栈工程师的自我修养>,颇有收获,故在此对读到的内容加以整理,方便指导,同时再回顾一遍书中的内容. 概览 整本书叙述的是作者的成长经历,通过经验的分享,给新人或者 ...
- 程序员的自我修养(2)——计算机网络(转) good
相关文章:程序员的自我修养——操作系统篇 几乎所有的计算机程序,都会牵涉到网络通信.因此,了解计算机基础网络知识,对每一个程序员来说都是异常重要的. 本文在介绍一些基础网络知识的同时,给出了一些高质量 ...
- GIS制图人员的自我修养(2)--制图意识
GIS制图人员的自我修养(2)--制图意识 by 李远祥 上次提及到GIS制图人员的一些制图误区,主要是为GIS制图人员剖析在制图工作中的一些问题.但如何提高制图的自我修养,却是一个非常漫长的过程,这 ...
- web性能优化 来自《web全栈工程师的自我修养》
最近在看<web全栈工程师的自我修养>一书,作者是来自腾讯的前端工程师.作者在做招聘前端的时候问应聘者web新能优化有什么了解和经验,应聘者思索后回答“在发布项目之前压缩css和 Java ...
- gcc ld 链接器相关知识,调试指令(程序员的自我修养----链接、装载与库)
最近解决一个动态链接上的问题,因为以前从来没有接触过这方面的知识,所以恶补了一下,首先要了解gcc编译指令(makefile),ld链接器的选项(还有连接脚本section指定内存位置),熟悉查看连接 ...
- Python学习笔记(四十九)爬虫的自我修养(一)
论一只爬虫的自我修养 URL的一般格式(带括号[]的为可选项): protocol://hostname[:port]/path/[;parameters][?query]#fragment URL由 ...
- Hacker的社交礼仪与自我修养【转】
Hacker School是位于纽约的一所特殊的编程“学校”,他们的目标是帮助参与者变成“更好的程序员”,之所以说他们特殊是因为这所“学校”没有老师,没有考试,也不会颁发证书,他们信奉三人行必有我师, ...
- 第八周读书笔记(人月神话X月亮与六便士)——到底什么才是一个程序员的自我修养?
写了这么久的读书笔记,涉及到问题大多是一些如何把软件工程做好,如何把自己的职业生涯做好.但总感觉逻辑链上缺了一环,亦即:我们为什么要把软件工程做好,我们成为一名优秀的职业生涯的意义到底在于什么?我觉得 ...
随机推荐
- 2015年蓝桥杯C/C++大学B组省赛真题(加法变乘法)
题目描述: 我们都知道:1+2+3+ ... + 49 = 1225 现在要求你把其中两个不相邻的加号变成乘号,使得结果为2015 比如: 1+2+3+...+10*11+12+...+27*28+2 ...
- 【python基础】编写/运行hello world项目
1.编写hello world项目 编程界每种语言的第一个程序往往都是输出hello world.因此我们来看看,如何用Python输出hello world. 1.如果你是初学者,main.py中的 ...
- 为什么 HashMap 会死循环?
HashMap 死循环发生在 JDK 1.8 之前的版本中,它是指在并发环境下,因为多个线程同时进行 put 操作,导致链表形成环形数据结构,一旦形成环形数据结构,在 get(key) 的时候就会产生 ...
- 【Haxe】(一)VSCode 搭建 Haxe 开发环境
前言 咱换工作啦! 新工作这边需要用到的开发语言是 Haxe,最近大概会写几篇笔记.Haxe 的介绍就不写了,打算记录点有用的学习内容,先从搭建开发环境开始吧! 当前适用版本: VSCode:Curr ...
- pip 20.3 新版本发布!即将抛弃 Python 2.x
据 Python 软件基金会消息,Python Packaging Authority 和 pip 团队于北美时间11月30日宣布发布 pip 20.3版本,开发者可以通过运行 python -m p ...
- NixOS 与 Nix Flakes 新手入门
独立博客阅读: https://thiscute.world/posts/nixos-and-flake-basics/ 长文警告️ 本文的目标 NixOS 版本为 22.11,Nix 版本为 2.1 ...
- 【Python入门教程】Python常用表格函数&操作(xlrd、xlwt、openpyxl、xlwings)
在我们使用Python时,避免不了与Excel打交道.同样Python的三方库和代码的简洁性也为我们处理大数据提供了便利.今天给大家介绍一下常用的处理表格的函数,同时还有一些常用的 ...
- Kubernetes(k8s)网络策略NetworkPolicy
目录 一.系统环境 二.前言 三.网络策略(NetworkPolicy)简介 四.创建pod和svc 五.没有网络策略的条件下访问pod 六.给pod添加网络策略 6.1 入站网络策略 6.1.1 入 ...
- memcached使用中踩的一些坑
背景 线上启用memcached(以下简称mc)作为热点缓存组件已经多年,其稳定性和性能都经历住了考验,这里记录一下踩过的几个坑. 大key存储 某年某月某日,观察mysql的读库CPU占比有些异常偏 ...
- 高并发场景下,6种解决SimpleDateFormat类的线程安全问题方法
摘要:解决SimpleDateFormat类在高并发场景下的线程安全问题可以有多种方式,这里,就列举几个常用的方式供参考. 本文分享自华为云社区<[高并发]更正SimpleDateFormat类 ...