王有志,一个分享硬核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. 2014年蓝桥杯C/C++大学B组省赛真题(奇怪的分式)

    题目描述: 上小学的时候,小明经常自己发明新算法.一次,老师出的题目是:1/4 乘以 8/5 小明居然把分子拼接在一起,分母拼接在一起,答案是:18/45 (参见图1.png)老师刚想批评他,转念一想 ...

  2. python -----类反射

    #反射#描述:反射就是指在程序运行时,动态的去确定对象的类型,并且可以通过字符串的形式去调用对应的属性# ,方法,导入模块,是一种基于字符串的事情驱动# class User:# def __init ...

  3. 新版idea快捷键总结学习----(用于java开发模式)

    选择代码区 ctrl w 如果放到以if开头的语句,可以选择if判断条件所在的代码片段 游标在单个单词下时 选择单词 在选中多个单词时,选择整个字符串 三次点击时,如果不在字符串单词下,用于选择{}内 ...

  4. Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

    系列文章目录和关于我 零丶背景 最近有很多想学的,像netty的使用.原理源码,但是苦于自己对于操作系统和nio了解不多,有点无从下手,遂学习之. 一丶网络io的过程 上图粗略描述了网络io的过程,了 ...

  5. Java 网络编程 —— RMI 框架

    概述 RMI 是 Java 提供的一个完善的简单易用的远程方法调用框架,采用客户/服务器通信方式,在服务器上部署了提供各种服务的远程对象,客户端请求访问服务器上远程对象的方法,它要求客户端与服务器端都 ...

  6. Centos6yum源切换

    CentOS 6操作系统版本结束了生命周期(EOL),Linux社区已不再维护该操作系统版本.建议您升级操作系统至CentOS 7及以上,如果您的业务过渡期仍需要使用CentOS 6系统中的一些安装包 ...

  7. 通过安装GVM 安装GO 操作步骤

    转载请注明出处: 1.GVM GVM是Go Version Manager的缩写,是一个用于管理Go语言版本的工具.通过GVM,我们可以轻松地安装.切换和卸载不同版本的Go语言.GVM会在用户的hom ...

  8. 创建springboot工程失败解决 spring initializr Error:cannot download

    创建springboot工程失败解决 问题描述 原因分析: 网络不好,因为springBooT项目的创建时必须联网的 解决方案: 方案一: 将创建 springBoot 工程的地址更换为如下的地址 阿 ...

  9. spring连接数据库mysql报错 state 08S01 com.mysql.jdbc.exceptions.jdbc4.CommunicationsException

    spring连接数据库mysql报错errorCode0,state08S01com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Comm ...

  10. 【智能安防】基于AI的智能家居安全系统设计与实现

    目录 智能家居安全系统设计与实现:AI技术的应用 摘要 随着智能家居市场的快速发展,安全问题也日益突出.本文将介绍基于AI的智能家居安全系统设计与实现技术,重点阐述相关概念.实现步骤和优化改进.通过实 ...