【并发编程】volatile实现原理解析

article/2024/3/2 11:05:56

📫作者简介:小明Java问道之路2022年度博客之星全国TOP3,专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化,文章内容兼具广度、深度、大厂技术方案,对待技术喜欢推理加验证,就职于知名金融公司后端高级工程师。

           

🏆 2022博客之星TOP3 | CSDN博客专家 | 后端领域优质创作者 | CSDN内容合伙人

🏆 InfoQ(极客邦)签约作者、阿里云专家 | 签约博主、51CTO专家 | TOP红人、华为云享专家

         

 🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~ 


🍅 文末获取联系 🍅  👇🏻 精彩专栏推荐订阅收藏 👇🏻

专栏系列(点击解锁)

学习路线(点击解锁)

知识定位

🔥Redis从入门到精通与实战🔥

Redis从入门到精通与实战

围绕原理源码讲解Redis面试知识点与实战

🔥MySQL从入门到精通🔥

MySQL从入门到精通

全面讲解MySQL知识与企业级MySQL实战

🔥计算机底层原理🔥

深入理解计算机系统CSAPP

以深入理解计算机系统为基石,构件计算机体系和计算机思维

Linux内核源码解析

围绕Linux内核讲解计算机底层原理与并发

🔥数据结构与企业题库精讲🔥

数据结构与企业题库精讲

结合工作经验深入浅出,适合各层次,笔试面试算法题精讲

🔥互联网架构分析与实战🔥

企业系统架构分析实践与落地

行业最前沿视角,专注于技术架构升级路线、架构实践

互联网企业防资损实践

互联网金融公司的防资损方法论、代码与实践

🔥Java全栈白宝书🔥

精通Java8与函数式编程

本专栏以实战为基础,逐步深入Java8以及未来的编程模式

深入理解JVM

详细介绍内存区域、字节码、方法底层,类加载和GC等知识

深入理解高并发编程

深入Liunx内核、汇编、C++全方位理解并发编程

Spring源码分析

Spring核心七IOC/AOP等源码分析

MyBatis源码分析

MyBatis核心源码分析

Java核心技术

只讲Java核心技术

本文目录

本文导读

一、什么是volatile

二、volatile 的使用场景

三、什么是 happens-before 关系

四、volatile 的原理

1、可见性(lock 前缀的指令)

2、禁止重排(内存屏障)

五、volatile 和 synchronized 的关系

六、单例双重检查锁模式中为什么要 volatile

总结


本文导读

本文深入浅出讲解了什么是volatile与volatile 的使用场景,对什么是 happens-before 关系进行不同场景分析,最后分析volatile 可见性(lock指令)与禁止重排(内存屏障)的原理,最后给出两道常见面试题“volatile 和 synchronized 的关系”、“单例双重检查锁模式中为什么要 volatile”作为本文扩展。

一、什么是volatile

volatile 是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是 volatile 修饰的变量对所有线程总数立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中。

好处:比 synchronized 的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。

volatile 保证可见性和禁止重排序,无法保证原子性。Java 提供 volatile 来保证一定的有序性。最著名的例子就是单例模式里面的双重检查锁。重排序它不会影响单线程的运行结果,但是对多线程会有影响。

二、volatile 的使用场景

通常情况,volatile 用来修饰 boolean 类型的标记位,因为对于标记位来讲,直接的赋值操作本身就是具备原子性的,再加上 volatile 保证了可见性,那么就是线程安全的了。

对于多个线程同时操作的的场景,不仅仅是一个简单的赋值操作,而是需要先读取当前的值,然后在此基础上进行一定的修改,再把它给赋值回去。这样一来,我们的 volatile 就不足以保证这种情况的线程安全了。

三、什么是 happens-before 关系

happens-before:该原则保证了程序的“有序性”,它规定如果两个操作的执行顺序无法从 happens-before 原则中推到出来,那么他们就不能保证有序性,可以随意进行重排序。

也就是说,一个线程对于变量的操作需要对另一线程对于变量的操作可见,一定存在happens-before 关系

· 程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

· 锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。

· volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。

· 线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
传递性 A先于B ,B先于C 那么A必然先于C

· 线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的 join方法成功返回后,线程B对共享变量的修改将对线程A可见。

· 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。

· 对象终结规则对象的构造函数执行,结束先于finalize()方法

四、volatile 的原理

1、可见性(lock 前缀的指令)

第一层的作用是保证可见性。Happens-before 关系中对于 volatile 是这样描述的:对一个 volatile 变量的写操作 happen-before 后面对该变量的读操作。

这就代表了如果变量被 volatile 修饰,那么每次修改之后,接下来在读取这个变量的时候一定能读取到该变量最新的值。

内存屏障(Memory Barrier)的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

对 Volatile 进行写操作 CPU 会在,有 volatile 变量修饰的共享变量进行写操作的时候,多lock汇编代码。

lock 前缀的指令在CPU中会引发了两件事情:一、将当前处理器缓存行的数据会写回到系统内存。二、这个写回内存的操作会引起在其他 CPU 里缓存了该内存地址的数据无效。

2、禁止重排(内存屏障)

第二层的作用就是禁止重排序。先介绍一下 as-if-serial语义:不管怎么重排序,(单线程)程序的执行结果不会改变。在满足 as-if-serial 语义的前提下,由于编译器或 CPU 的优化,代码的实际执行顺序可能与我们编写的顺序是不同的,这在单线程的情况下是没问题的,但是一旦引入多线程,这种乱序就可能会导致严重的线程安全问题。

用了 volatile 关键字就可以在一定程度上禁止这种重排序

volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化:

1、在每个volatile写操作的前面插入一个StoreStore,使对于变量写操作的后面的写操作不会重排序到前面

2、在每个volatile写操作的后面插入一个StoreLoad,使对于变量读操作的后面的写操作不会重排序到前面

3、在每个volatile读操作的后面插入一个LoadLoad,使对于变量读操作的后面的读操作不会重排序到前面

4、在每个volatile读操作的后面插入一个LoadStore,使对于变量写操作的后面的读操作不会重排序到前面

五、volatile 和 synchronized 的关系

相似性:volatile 可以看作是一个轻量版的 synchronized,比如一个共享变量如果自始至终只被各个线程赋值和读取,而没有其他操作的话,那么就可以用 volatile 来代替 synchronized 或者代替原子变量,足以保证线程安全。

实际上,对 volatile 字段的每次读取或写入都类似于“半同步”——读取 volatile 与获取 synchronized 锁有相同的内存语义,而写入 volatile 与释放 synchronized 锁具有相同的语义。

性能方面:volatile 属性的读写操作都是无锁的,正是因为无锁,所以不需要花费时间在获取锁和释放锁上,所以比 synchronized 性能更好。

六、单例双重检查锁模式中为什么要 volatile

public class Singleton {private static volatile Singleton singleton;public static Singleton getInstance() {if (singleton == null) {synchronized (Singleton.class) {if (singleton == null) {singleton = new Singleton();}}}return singleton;}
}

主要就在于 singleton = new Singleton() ,它并非是一个原子操作,在 JVM 中上述语句至少做了以下这 3 件事,因为存在指令重排序的优化,也就是说第2 步和第 3 步的顺序是不能保证的。

总结

本文深入浅出讲解了什么是volatile与volatile 的使用场景,对什么是 happens-before 关系进行不同场景分析,最后分析volatile 可见性(lock指令)与禁止重排(内存屏障)的原理,最后给出两道常见面试题“volatile 和 synchronized 的关系”、“单例双重检查锁模式中为什么要 volatile”作为本文扩展。


http://www.ngui.cc/article/show-1753744.html

相关文章

go使用aes加密算法

工具代码 package toolimport ("bytes""crypto/aes""crypto/cipher" )// AES加密函数 var key []byte []byte("0#3456789ABCDEF") //todo 记住这个长度只能是16 24 32 如果不是的话话会报错 func Encrypt(data []byte) ([]byte, er…

opencv学习二:加载显示图片

文章目录 加载显示图片(一)函数1.imread()读取图片(1)matplotlib和opencv中imread函数的区别 加载显示图片 (一)函数 1.imread()读取图片 Mat imread(const string& filename, int flags1 );第一个参…

深入理解Java中继承的高级使用方案

摘要: 继承是Java中的一项强大的特性,它允许子类从父类中继承属性和方法。然而,继承的高级使用方案涉及更复杂的概念和技术,可以帮助开发人员构建更加灵活、可维护和可扩展的代码。本文将深入探讨Java中继承的高级用法&#xff0c…

MDK5改造之格式化以及文件函数注释插件和主题应用

MDK5插件以及主题应用 前言一、主题修改1、主题文件下载2、主题应用二、插件安装以及使用1.下载插件2、插件使用步骤前言 为了写代码的心应手,先对MDK5进行改造 提示:以下是本篇文章正文内容,下面案例可供参考 🎉参考了其他大师的文章,链接如下: MDK5插件:代码格式整理、…

如何在WordPress中批量替换图片路径?

很多站长在使用WordPress博客或者搬家时,需要把WordPress文章中的图片路径进行替换来解决图片不显示的问题。总结一下WordPress图片路径批量替换的过程,方便有此类需求的站长们学习。 什么情况下批量替换图片路径 1、更换了网站域名 有许多网站建设初期…

uniapp 微信小程序连接蓝牙卡死

解决方法,需要同意隐私保护协议,否则不能开启蓝牙权限和定位权限,会导致定位失败

制作一个RISC-V的操作系统二-RISC-V ISA介绍

文章目录 ISA的基本介绍啥是ISA为什么要设计ISACISCvsRISCISA的宽度知名ISA介绍 RISC-V历史和特点RISC-V发展RISC-V ISA 命名规范模块化的ISA通用寄存器Hart特权级别Control and Status Register(CSR)内存管理与保护异常和中断 ISA的基本介绍 啥是ISA …

css如何设置文本添加下划线

css文本添加下划线 text-decoration: underline;text-decoration相关属性参数 参数描述none默认。定义标准的文本。underline定义文本下的一条线。overline定义文本上的一条线。line-through定义穿过文本下的一条线。blink定义闪烁的文本。inherit规定应该从父元素继承 text-…

Ext4文件系统解析(一)

1、前言 熟悉Linux操作系统的都应该或多或少的了解或者使用过Ext4文件系统。 接下来,会简单介绍Ext4文件系统的一些特性和工作原理。 2、常用概念 在介绍Ext文件系统之前,先简单描述一些相关概念。 块(Block):Ext文件系统存储分配的基本单…

反转链表的实现

题目描述: 给出一个链表的头节点,将其反转,并返回新的头节点 思路1:反转地址 将每个节点里的地址由指向下一个节点变为指向前一个节点 定义三个结构体指针n1,n2,n3,n1表示改后指针的地址,n2表示要修改结构体里next的…