王有志,一个分享硬核Java技术的互金摸鱼侠

加入Java人的提桶跑路群:共同富裕的Java人

今天是《面霸的自我修养》第4篇文章,我们一起来看看面试中会问到哪些关于volatile的问题吧。

数据来源:

  • 大部分来自于各机构(Java之父,Java继父,某灵,某泡,某客)以及各博主整理文档;
  • 小部分来自于我以及身边朋友的实际经理,题目上会做出标识,并注明面试公司。

叠“BUFF”:

  • 八股文通常出现在面试的第一二轮,是“敲门砖”,但仅仅掌握八股文并不能帮助你拿下Offer;
  • 由于本人水平有限,文中难免出现错误,还请大家以批评指正为主,尽量不要喷~~
  • 本文及历史文章已经完成PDF文档的制作,提取关键字【面霸的自我修养】。

理论篇

指令重排

难易程度

重要程度

面试公司:无

指令重排是一种优化技术,通过指令乱序执行(Out Of Order Execution,简称OoOE或OOE)提高处理器的执行效率和性能

以下内容摘自维基百科:

在计算机工程领域,乱序执行错序执行,英语:out-of-order execution,简称OoOEOOE)是一种应用在高性能微处理器中来利用指令周期以避免特定类型的延迟消耗的范式。在这种范式中,处理器根据输入数据的可用性确定执行指令的顺序,而不是根据程序的原始数据决定。在这种方式下,可以避免因为获取下一条程序指令所引起的处理器等待,取而代之的处理下一条可以立即执行的指令。

指令重排的基础建立在保证当线程环境下语义准确性的前提下,而不能保证多线程环境下的语义。


内存屏障

难易程度

重要程度

面试公司:无

内存屏障(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();
}

删除修饰flagvolatile后,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行的两个指令:putstaticgetstatic(非静态变量对应putfieldgettfield)用于操作静态变量instance,这两条指令的源码位于bytecodeInterpreter中,以下仅截取关键部分源码。

volatile变量的写操作

putstaticputfield指令:

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变量的读操作

getstaticgettfield指令:

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变量的getstaticgettfield指令需要使用OrderAccess::fence来保证语义的正确性,否则不需要使用。

Tips:文末参考资料中提供了关于IRIW和MCA模型的部分资料,感兴趣的可以自行阅读。


synchronized和volatile有哪些区别?

难易程度

重要程度

面试公司:无

synchronizedvolatile都是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协议等。

参考资料


如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠王有志,我们下次再见!

面霸的自我修养:volatile专题的更多相关文章

  1. GIS制图人员的自我修养(1)--制图误区

    GIS制图人员的自我修养 by 李远祥 最近一直坚持写GIS制图的技术专题,并不是为了要介绍有什么好的技术和方法去制图,而是要告诉所有从事这一方向的人员一个铁铮铮的实现--要做好GIS制图,必须加强自 ...

  2. IT技术管理者的自我修养

    1. 前言 本来写<IT技术管理者的自我修养>与<IT技术人员的自我修养>是一开始就有的想法.但发表<IT技术人员的自我修养>后,收到了不少良好的反馈,博客园的编辑 ...

  3. 《web全栈工程师的自我修养》读书笔记

    有幸读了yuguo<web全栈工程师的自我修养>,颇有收获,故在此对读到的内容加以整理,方便指导,同时再回顾一遍书中的内容. 概览 整本书叙述的是作者的成长经历,通过经验的分享,给新人或者 ...

  4. 程序员的自我修养(2)——计算机网络(转) good

    相关文章:程序员的自我修养——操作系统篇 几乎所有的计算机程序,都会牵涉到网络通信.因此,了解计算机基础网络知识,对每一个程序员来说都是异常重要的. 本文在介绍一些基础网络知识的同时,给出了一些高质量 ...

  5. GIS制图人员的自我修养(2)--制图意识

    GIS制图人员的自我修养(2)--制图意识 by 李远祥 上次提及到GIS制图人员的一些制图误区,主要是为GIS制图人员剖析在制图工作中的一些问题.但如何提高制图的自我修养,却是一个非常漫长的过程,这 ...

  6. web性能优化 来自《web全栈工程师的自我修养》

    最近在看<web全栈工程师的自我修养>一书,作者是来自腾讯的前端工程师.作者在做招聘前端的时候问应聘者web新能优化有什么了解和经验,应聘者思索后回答“在发布项目之前压缩css和 Java ...

  7. gcc ld 链接器相关知识,调试指令(程序员的自我修养----链接、装载与库)

    最近解决一个动态链接上的问题,因为以前从来没有接触过这方面的知识,所以恶补了一下,首先要了解gcc编译指令(makefile),ld链接器的选项(还有连接脚本section指定内存位置),熟悉查看连接 ...

  8. Python学习笔记(四十九)爬虫的自我修养(一)

    论一只爬虫的自我修养 URL的一般格式(带括号[]的为可选项): protocol://hostname[:port]/path/[;parameters][?query]#fragment URL由 ...

  9. Hacker的社交礼仪与自我修养【转】

    Hacker School是位于纽约的一所特殊的编程“学校”,他们的目标是帮助参与者变成“更好的程序员”,之所以说他们特殊是因为这所“学校”没有老师,没有考试,也不会颁发证书,他们信奉三人行必有我师, ...

  10. 第八周读书笔记(人月神话X月亮与六便士)——到底什么才是一个程序员的自我修养?

    写了这么久的读书笔记,涉及到问题大多是一些如何把软件工程做好,如何把自己的职业生涯做好.但总感觉逻辑链上缺了一环,亦即:我们为什么要把软件工程做好,我们成为一名优秀的职业生涯的意义到底在于什么?我觉得 ...

随机推荐

  1. phpstudy-pikachu-字符型注入(get)

    在查询栏输入1,点击查询获得查询格式 ?name=1'&submit=查询 *捷径 ' or 1=1 --+ *非捷径 ?name=1' and 1=2 --+ 2--+&submit ...

  2. OneForAll下载安装以及环境配置

    python-3.9.7-amd64 OneForAll-master python安装以及插件安装 首先下载python解压到电脑c盘在c盘中创建一个工具文件夹,然后下载OneForAll-mast ...

  3. 基于Electron24+Vite4+Vue3搭建桌面端应用

    一说到创建桌面应用,就不得不提及Electron和Tauri框架.这次给大家主要分享的是基于electron最新版本整合vite4.x构建vue3桌面端应用程序. 之前也有使用vite2+vue3+e ...

  4. SQL后半部和JDBC

    SQL后半部 排序order by asc 升序desc 降序select *from 表名 order by 列名 asc ; select *from 表名 order by 列名 asc , 列 ...

  5. 如何在 Python 中实现遗传算法

    前言 遗传算法是一种模拟自然进化过程与机制来搜索最优解的方法,它由美国 John Holland 教授于20世纪70年代提出.遗传算法的主要思想来源于达尔文生物进化论和孟德尔的群体遗传学说,通过数学的 ...

  6. .NET周报 【6月第3期 2023-06-18】

    国内文章 揭秘 Task.Wait https://www.cnblogs.com/eventhorizon/p/17481757.html Task.Wait 是 Task 的一个实例方法,用于等待 ...

  7. C++ Tips of the Week-01 string_view

    1: string_view ref https://abseil.io/tips/1 函数参数需要是一个字符串的时候,通常的实现有以下几种: // C Convention void TakesCh ...

  8. 解决Mysql 5.7 不能插入中文的问题

    问题的解决方案 问题描述 : 在学习DML插入中文数据时 , 发现出现了以下问题 -- 插入数据 insert into tea (id , name) values (2 , '徐凤年'); -- ...

  9. 生成式预训练Transformer:探索其在自然语言处理领域的最新应用

    目录 1. 引言 2. 技术原理及概念 3. 实现步骤与流程 4. 应用示例与代码实现讲解 生成式预训练Transformer:探索其在自然语言处理领域的最新应用 1. 引言 自然语言处理 (NLP) ...

  10. Django自身提供测试类、工具-调研

    Django自身提供测试类.工具 django.test.Client 他的作用是模拟客户端.提供一系列的方法,例如get.post.delete.login等其中login是用django自身的验证 ...