volatile考点分析

今天我们学习并发编程中另一个重要的关键字volatile,虽然面试中它的占比低于synchronized,但依旧是不可忽略的内容。

关于volatile,我收集到了8个常见考点,围绕应用,特点和实现原理。

  1. volatile有什么作用?
  2. 为什么多线程环境中会出现可见性问题?
  3. synchronized和volatile有哪些区别?
  4. 详细描述volatile的实现原理(涉及内存屏障)。
  5. volatile有哪些特性?它是如何保证这些特性的?
  6. volatile保证线程间变量的可见性,是否意味着volatile变量就是并发安全的?
  7. 为什么方法中的变量不需要使用volatile?
  8. 重排序是如何产生的?

本文从volatile应用开始,接着从源码角度分析volatile的实现,通过对原理的剖析尝试解答以上问题。

volatile是什么

同synchronized一样,volatile是Java的提供的用于并发控制的关键字,不过它们之间也有比较明显的差异。

首先是使用方式:

  • synchronized能够修饰方法和代码块
  • volatile只能修饰成员变量

能力上volatile也更“弱”一些:

  • 保证被修饰变量的可见性
  • 禁止被修饰变量发生指令重排

我们稍微修改关于线程你必须知道的8个问题(上)中可见性问题的代码,使用volatile修饰变量flag:

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();
}

不难发现,block_thread解脱了,说明对flag的修改被其它线程“看见了”,这就是volatile保证可见性的表现。

接着修改深入理解JMM和Happens-Before中指令重排带来有序性问题的代码,同样使用volatile修饰变量instance:

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;}
}

多次实验后发现,不会再获取到未经初始化的instacne对象了,这就是volatile禁止指令重排的表现。

Tips:再次强调,Happens-Before描述的是行为结果间的关系

volatile的实现

以下内容基于JDK 11 HotSpot虚拟机,以X86架构的实现为主,会与ARM架构的实现对比。选择这些的原因很简单,它们是各自领域的“顶流”

volatile使用简单,功能易理解,但往往简单的背后隐藏着复杂的实现。和分析synchronized的过程一样,从字节码开始,再到JVM的实现,力求从底层串联volatile,内存屏障与硬件之间的关系。

volatile在不同架构下的实现差异较大,看个例子,X86架构的templateTable_x86__中getfield_or_static方法的实现:

void TemplateTable::getfield_or_static(int byte_no, bool is_static, RewriteControl rc) {// 省略类型判断的代码__ bind(Done);// [jk] not needed currently// volatile_barrier(Assembler::Membar_mask_bits(Assembler::LoadLoad | Assembler::LoadStore));
}

ARM架构的templateTable_arm__中getfield_or_static方法的实现:

void TemplateTable::getfield_or_static(int byte_no, bool is_static, RewriteControl rc) {// 省略类型判断的代码__ bind(Done);if (gen_volatile_check) {Label notVolatile;__ tbz(Rflagsav, ConstantPoolCacheEntry::is_volatile_shift, notVolatile);volatile_barrier(MacroAssembler::Membar_mask_bits(MacroAssembler::LoadLoad | MacroAssembler::LoadStore), Rtemp);__ bind(notVolatile);}
}

X86架构下不需要对volatile类型进行特殊处理,而ARM架构下,添加内存屏障保证了与X86架构一致的效果。这个例子是为了展示规范在不同CPU架构上的实现差异,另外提醒大家不要误将X86的实现当成标准,X86架构对重排序的约束更强,能“天然”实现JMM规范中的某些要求,所以JVM层面的实现看起来会非常简单。

上面一直在说模板解释器,不过后面的内容我要用字节码解释器bytecodeInterpreter了。为什么不用模板解释器?因为模板解释器离OrderAccess太“远”了,而OrderAccess中内存屏障的详细解释是理解volatile原理的关键。

不过,我们还是先花点时间了解下X86架构下内存屏障assembler_x86的实现:

enum Membar_mask_bits {
StoreStore = 1 << 3,
LoadStore  = 1 << 2,
StoreLoad  = 1 << 1,
LoadLoad   = 1 << 0
};void membar(Membar_mask_bits order_constraint) {if (os::is_MP()) {if (order_constraint & StoreLoad) {int offset = -VM_Version::L1_line_size();if (offset < -128) {offset = -128;}lock();addl(Address(rsp, offset), 0);}}
}

使用位掩码定义内存屏障的枚举,分析偏向锁的时候就见到过位掩码的使用,重点在membar方法中最后两行代码:

lock();
addl(Address(rsp, offset), 0);

插入了lock addl指令,它是X86架构下内存屏障实现的关键,orderAccess_linux_x86中的实现也是如此。

Tips:membar方法是Memory Barrier(内存屏障)的缩写,另外也有称为Memory Fence(内存栅栏)的,或者直接称为fence,反正屏障,栅栏什么的乱七八糟的。

从字节码开始

使用双检锁单例模式生成的字节码:

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;

我们看字节码中的关键部分:

  • 标记volatile变量的ACC_VOLATILE;
  • 写入/读取静态变量时的指令getstatic和putstatic。

Java 11虚拟机规范第4章中是这样描述ACC_VOLATILE的:

ACC_STATIC          0x0008      Declared static. ACC_VOLATILE     0x0040      Declared volatile; cannot be cached.

虚拟机规范要求被volatile修饰的变量不能被缓存。我们知道,CPU高速缓存是带来可见性问题的“罪魁祸首”,不能被缓存就意味着杜绝了可见性问题,但并不意味着不使用缓存。

Java 11虚拟机规范第6章中也描述了getstatic指令的作用:

Get static field from class.

putstatic指令的作用:

Set static field in class.

可以大致猜到JVM的实现volatile的方式,JVM中定义getstatic/putstatic指令对应的方法,并在方法中判断变量是否被标记为ACC_VOLATILE,然后进行特殊逻辑处理。

Tips

  • 0x0048是ACC_STATIC和ACC_VOLATILE结合的结果;
  • 非static变量,读取和写入是getfield和putfield两条指令。

字节码解释器的实现

这部分我们只看putstatic的源码,前面模板解释器的部分也大致分析了getstatic,剩下的就留给大家自行分析了。

putstatic的实现在bytecodeInterpreter中第2026行:

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变量的处理方式}
}

逻辑很简单,判断变量的类型,然后调用OrderAccess::storeload(),保证volatile变量的特性实现。

JVM的内存屏障

JVM在不同操作系统,CPU架构的基础上,构建了一套符合JMM规范的内存屏障,屏蔽了不同架构间的差异,实现了内存屏障的语义一致性。这部分重点解释JVM实现的4种主要内存屏障和介绍X86架构的实现以及硬件差异导致的不同。

来看orderAccess中对4种内存屏障的解释:

Memory Access Ordering Model

LoadLoad: Load1(s); LoadLoad; Load2

Ensures that Load1 completes (obtains the value it loads from memory) before Load2 and any subsequent load operations. Loads before Load1 may not float below Load2 and any subsequent load operations.

StoreStore: Store1(s); StoreStore; Store2

Ensures that Store1 completes (the effect on memory of Store1 is made visible to other processors) before Store2 and any subsequent store operations.  Stores before Store1 may not float below Store2 and any subsequent store operations.

LoadStore: Load1(s); LoadStore; Store2

Ensures that Load1 completes before Store2 and any subsequent store operations.  Loads before Load1 may not float below Store2 and any subsequent store operations.

StoreLoad: Store1(s); StoreLoad; Load2

Ensures that Store1 completes before Load2 and any subsequent load operations.  Stores before Store1 may not float below Load2 and any subsequent load operations.

努力翻译下对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指令后。

虽然翻译过来有些拗口,但理解起来并不困难,建议小伙伴们认真阅读这部分注释(包括后面的内容)。

注释中可以看出,内存屏障保证了程序的有序性

X86架构的内存屏障实现

Linux平台X86架构的实现orderAccess_linux_x86中对内存屏障的定义:

inline void OrderAccess::loadload()   { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore()  { compiler_barrier(); }
inline void OrderAccess::storeload()  { fence();            }
inline void OrderAccess::acquire()    { compiler_barrier(); }
inline void OrderAccess::release()    { compiler_barrier(); }

实现非常简单,只有两个核心方法compiler_barrier和fence:

static inline void compiler_barrier() {__asm__ volatile ("" : : : "memory");}inline void OrderAccess::fence() {// always use locked addl since mfence is sometimes expensive#ifdef AMD64__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");#else__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");#endifcompiler_barrier();}

上述代码是GCC的扩展内联汇编形式,简单解释下compiler_barrier方法中的内容:

  • asm,插入汇编指令;
  • volatile,禁止优化此处的汇编指令;
  • meomory,汇编指令修改了内存,要重新读取内存数据。

接着是支撑storeload屏障的fence方法,和templateTable_x86的实现一样,核心是lock addl指令。lock前缀指令可以理解为CPU指令级的锁,对总线和缓存加锁,主要有两个作用:

  • Lock前缀指令会引起处理器缓存回写到内存
  • 处理器缓存回写到内存会导致其他处理器的缓存无效

实际上X86架构提供了内存屏障指令lfence,sfence,mfence等,但为什么不使用内存屏障指令呢?原因在fence的注释中:

mfence is sometimes expensive

即mfence指令在性能上的开销较大。好了,到这里我们已经能够得到X86架构下实现volatile特性的原理:

  • JVM的角度看,内存屏障提供了可见性和有序性的保证
  • X86的角度看,voaltile指令禁止重排序,Lock指令引起缓存失效和回写

Tips:fence方法中AMD64和X86的处理略有差异,关于它们的渊源,可以参考pansz大佬的知乎。

其他架构实现差异的原因

前面看到,X86架构和ARM架构的模板解释器中,getfield_or_static方法在使用内存屏障上产生了分歧,ARM通过内存屏障到达了“罗马”,而X86出生在“罗马”。

不难想到产生这种差异的原因,CPU架构对重排序的约束不同,导致JVM需要使用不同的处理方式达到统一的效果。关于CPU允许的重排序,我“搬运”了一张图:

该图来自介绍CPU缓存与内存屏障的经典文章《Memory Barriers:a Hardware View for Software Hackers》,虽然年代较为“久远”,但依旧值得阅读。原图中列标题是竖向,看起来并不方便,所以进行了简单的“视觉优化”。

从图中也可以看到,X86架构只允许“Stores Reordered After Loads”重排序,因此JVM中只对storeload进行了实现,至于其它屏障的特性则是由CPU自己保证的。

结语

volatile的内容太难写了,特性不难,源码也不难,但是讲内存屏障非常难。

说少了难以理解,说多了就“越界”,就成了写硬件的文章。因此在写硬件实现差异的策略是桌面端最常用的X86架构为主,并对比移动端最常用ARM架构的实现,尽量简短的解释volatile的实现。

实际上,内存屏障的部分还有acquirerelease两种单向屏障没有涉及到,大家可以自行了解。

那么,现在回到开始的题目中,相信你能够轻松的回答出前6道题目了吧?


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/57407.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PHP8内置函数中的数学函数-PHP8知识详解

php8中提供了大量的内置函数&#xff0c;以便程序员直接使用常见的内置函数包括数学函数、变量函数、字符串函数、时间和日期函数等。今天介绍内置函数中的数学函数。 本文讲到了数学函数中的随机数函数rand()、舍去法取整函数floor()、向上取整函数 ceil()、对浮点数进行四舍…

基于HarmonyOS ArkUI实现七夕壁纸轮播

七夕情人节&#xff0c;为了Ta&#xff0c;你打算用什么方式表达爱&#xff1f;是包包、鲜花、美酒、巧克力&#xff0c;还是一封充满爱意的短信&#xff1f;作为程序员&#xff0c;以代码之名&#xff0c;表达爱。本节将演示如何在基于HarmonyOS ArkUI的SwiperController、Ima…

CrystalNet .Net VCL for Delphi Crack

CrystalNet .Net VCL for Delphi Crack VCL或更为人所知的可视化组件库是基于一个面向对象的框架&#xff0c;什么是用户对开发人员和事件的Microsoft Windows应用程序的接口。可视化组件库是用对象Pascal编写的。它主要是为使用Borland而开发的&#xff0c;它具有与Delphi以及…

释放 ChatGPT 的价值:5 个专家提示

随着近来ChatGPT的热议&#xff0c;人工智能技术被推上风口浪尖&#xff0c;由此以数字化技术为基础的数字营销也再次受到了不小的关注&#xff0c;但是营销的本质从来都没有变过&#xff0c;今天我们聊下ChatGPT无论如何演进&#xff0c;人工智能无论变得多么先进&#xff0c;…

【C语言基础】const关键词的使用方法

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

django中使用websocket

python本身只支持http协议 使用websocket需要下载第三方库 pip install -U channels 需要在seting.py里配置&#xff0c;将我们的channels加入INSTALLED_APP里。 INSTALLED_APPS ( django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, …

【环境配置】Android-Studio-OpenCV-JNI以及常见错误 ( 持续更新 )

最近一个项目要编译深度学习的库&#xff0c;需要用到 opencv 和 JNI&#xff0c;本文档用于记录环境配置中遇到的常见错误以及解决方案 Invalid Gradle JDK configuration found failed Invalid Gradle JDK configuration foundInvalid Gradle JDK configuration found. Open…

Docker数据管理(数据卷与数据卷容器)

目录 一、数据卷&#xff08;Data Volumes&#xff09; 1、概述 2、原理 3、作用 4、示例&#xff1a;宿主机目录 /var/test 挂载同步到容器中的 /data1 二、数据卷容器&#xff08;DataVolumes Containers&#xff09; 1、概述 2、作用 3、示例&#xff1a;创建并使用…

Flutter(九)Flutter动画和自定义组件

目录 1.动画简介2.动画实现和监听3. 自定义路由切换动画4. Hero动画5.交织动画6.动画切换7.Flutter预置的动画过渡组件自定义组件1.简介2.组合组件3.CustomPaint 和 RenderObject 1.动画简介 Animation、Curve、Controller、Tween这四个角色&#xff0c;它们一起配合来完成一个…

AIGC - 生成模型

AIGC - 生成模型 0. 前言1. 生成模型2. 生成模型与判别模型的区别2.1 模型对比2.2 条件生成模型2.3 生成模型的发展2.4 生成模型与人工智能 3. 生成模型示例3.1 简单示例3.2 生成模型框架 4. 表示学习5. 生成模型与概率论6. 生成模型分类小结 0. 前言 生成式人工智能 (Generat…

【Android】TextView适配文本大小并保证中英文内容均在指定的UI 组件内部

问题 现在有一个需求&#xff0c;在中文环境下textView没有超过底层的组件限制&#xff0c;但是一切换到英文环境就超出了&#xff0c;这个如何解决呢&#xff1f;有啥例子吗&#xff1f; 就像这样子的。 解决 全部代码如下&#xff1a; <?xml version"1.0"…

JVM 判定对象是否死亡的两种方式

引用计数法&#xff1a;&#xff08;脑门刻字法&#xff09;和 可达性分析 引用计数算法 引用计数器的算法是这样的&#xff1a;在对象中添加一个引用计数器&#xff0c;每当有一个地方引用它时&#xff0c;计数器值就加一&#xff1b;当引用失效时&#xff0c;计数器值就减一…

使用 MATLAB 和 Simulink 对雷达系统进行建模和仿真

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

python使用 flask+vue 制作前后端分离图书信息管理系统

目录标题 前言制作前后端分离图书信息管理系统的思路&#xff1a;素材代码效果展示 后端部分接口部分前端部分尾语 前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 哈喽兄弟们&#xff0c;今天咱们来用Python实现一个前后端分离的图书信息管理系统。 制作前后端分离图书信…

葡萄叶病害识别(图像连续识别和视频识别,Python代码,pyTorch框架)

葡萄叶病害识别&#xff08;图像连续识别和视频识别&#xff0c;Python代码&#xff0c;pyTorch框架&#xff09;_哔哩哔哩_bilibili 葡萄数据集 第一个文件夹为 Grape Black Measles&#xff08;葡萄黑麻疹&#xff09;病害&#xff08;3783张&#xff09; Grape Black rot葡…

【优化算法】Python实现面向对象的遗传算法

遗传算法 遗传算法(Genetic Algorithm)属于智能优化算法的一种&#xff0c;本质上是模拟自然界中种群的演化来寻求问题的最优解。与之相似的还有模拟退火、粒子群、蚁群等算法。 在具体介绍遗传算法之前&#xff0c;我们先来了解一些知识&#x1f9c0; DNA&#xff1a; 携带有…

[FPGA IP系列] BRAM IP参数配置与使用示例

FPGA开发中使用频率非常高的两个IP就是FIFO和BRAM&#xff0c;上一篇文章中已经详细介绍了Vivado FIFO IP&#xff0c;今天我们来聊一聊BRAM IP。 本文将详细介绍Vivado中BRAM IP的配置方式和使用技巧。 一、BRAM IP核的配置 1、打开BRAM IP核 在Vivado的IP Catalog中找到B…

【腾讯云 TDSQL-C Serverless 产品测评】- 云原生时代的TDSQL-C MySQL数据库技术实践

一、活动介绍&#xff1a; “腾讯云 TDSQL-C 产品测评活动”是由腾讯云联合 CSDN 推出的针对数据库产品测评及产品体验活动&#xff0c;本次活动主要面向 TDSQL-C Serverless版本&#xff0c;初步的产品体验或针对TDSQL-C产品的自动弹性能力、自动启停能力、兼容性、安全、并发…

【uniapp】this有时为啥打印的是undefined?(箭头函数修改this)

&#x1f609;博主&#xff1a;初映CY的前说(前端领域) ,&#x1f4d2;本文核心&#xff1a;uniapp中this指向问题 前言&#xff1a;this大家知道是我们当前项目的实例&#xff0c;我们可以在这个this上面拿到我们原型上的全部数据。这个常用在我们在方法中调用其他方法使用。 …

STM32 无法烧录

1. 一直显示芯片没连接上&#xff0c;检查连线也没问题&#xff0c;换了个ST-Link 烧录器还是连不上&#xff0c;然后又拿这个烧录器去其它板子上试下&#xff0c;就可以连接上&#xff0c;说明我连线没问题&#xff0c;烧录器也没问题&#xff0c;驱动什么的更是没问题&#x…