【Java 并发编程】(三) 从CPU缓存开始聊 volatile 底层原理
并发编程
三大问题
- 在并发编程中,原子性、有序性和可见性是三个重要的问题,解决这三个问题是保证多线程程序正确性的基础。
- 原子性: 指的是一个操作不可分割, 要么全部执行完成, 要么不执行, 不存在执行一部分的情况.
- 有序性: 有序性是指程序的执行顺序与程序中代码的顺序一致。在多线程环境下,由于线程的交替执行和指令重排等因素,可能会导致代码的执行顺序与预期不一致
- 可见性: 可见性是指当一个线程修改了共享变量的值时,其他线程能够立即看到这个修改。在多核处理器和多级缓存系统中,线程对共享变量的修改可能被缓存到CPU的本地缓存中,而其他CPU上的线程无法立即看到这个修改,从而导致数据不一致的问题。
- 原子性问题, 可以用 synchronized 关键字解决, JDK 也提供了 ReentrantLock 等机制, 也能解决;
- 有序性和可见性, 可以由 volatile 解决;
原子性问题, 就不多说了, 下面重点介绍一下如何解决有序性和可见性问题;
HappensBefore原则
- 它是一种顺序保证,确保在并发环境下的有序性和可见性;
- 例如, 该规则规定同一个线程内的每个操作, 都 happens-before 于该线程中的任意后续操作;
- 例如, 一个监视器锁上一次的解锁操作, happens-before于下一次的加锁操作;
- 例如, 如果线程A调用线程B的start()方法来启动线程B,则start()操作Happens-Before于线程B中的任意操作。
- 如果 A happens before B, 那么 A 应该在 B 之前执行, 且 A 的结果应该对 B 可见;
原则固然好, 问题是怎么实现呢? 主要就是 synchronized + volatile, synchronized已经介绍过, 这里介绍 volatile, 让我们先从缓存开始说起;
缓存行
-
现代计算机为了缓和 CPU 速度和内存速度之间的差异, 会在内存与CPU之间设置多级缓存, 缓存的速度比内存快;
-
当 CPU 读缓存未命中时, 会从内存读取数据并放入到缓存中, 以后就可以直接从缓存中读取, 提高了速度;
-
从内存往 CPU Cache 读的时候, 根据程序局部性原理, 会按块(在缓存里也叫缓存行)读取, 大小为64B;
-
如果你有一个特别热点的变量, 那应该让他尽量独占一个缓存行, 怎么做? 在前后填充无意义数据, 前后都填充 7 * 8B, 这样就保证热点变量一定独占一个缓存行;
-
现在 CPU 一般都是多核的, 每个核相当于一个独立的 CPU;
-
现在的 CPU 缓存一般是三级缓存, 一二级在 CPU 核心内部, 三级共享;
-
补充: 超线程
一个CPU内有一套ALU计算单元, 两套程序计数器PC和寄存器, 这样就可以同时保存两个线程的上下文, 切换时只需要让ALU切换一下数据来源即可;
这样提高了线程切换效率, 8核16线程就是这么来的;
英特尔X86 - MESI
-
每个CPU内核有自己的 cache, 为了解决不同 CPU内核的 Cache 之间以及 Cache 与主存之间的一致性问题, 引入了串行总线 + MESI协议的解决方式;
-
将 Cache 中缓存的数据分为四种状态;
- **Exclusive(E):**当某个缓存数据仅存在于一个CPU核内, 并且与内存中的值一致时, 该缓存行的状态为 Exclusive。
- Modified(M): 在E的基础上, 如果内核修改了缓存, 使得与内存不一致, 该缓存行的状态为 Modified;
- Shared(S): 当一个缓存行被多个CPU内核缓存,并且缓存中的数据与内存中的数据一致时,该缓存行的状态为 Shared;
- Invalid(I): 在S的基础上, 某个内核修改自己的缓存时, 其它内核的缓存将被失效, 状态变为 Invalid
-
举例
-
CPU0 读变量a, a 从主存缓存到CPU0, 状态为 E;
-
CPU0 写变量a, 缓存状态改为 M;
-
CPU1 读变量a, 发现 CPU0 有变量a的缓存, 那么拿到自己的缓存里来, 并将缓存的最新值写入内存, 缓存的状态变为S, CPU0和1现在都有a的缓存, 且状态都是 S;
-
CPU0 再次修改a, 这会将 CPU1 的缓存失效, 状态改为 I; 并将最新值写入内存, CPU0自己的缓存状态改为 E;
-
-
当CPU内核去查询其它CPU内核是否有相同缓存, 以及通知其它CPU缓存失效等操作时, 为了避免这些操作发生混乱, 总线是串行的;
-
补充: 如果数据非常大, 一个缓存行放不下, 怎么保证一致性? 直接到内存中访问, 并且访问时锁总线;
store buffer & invalidate queue
-
因为总线是串行的, 所以效率较低, 为此引入了
store buffer
和invalidate queue
; 以下简称 SB 和 IQ; -
每个 CPU 都有自己 SB 和 IQ ;
-
前面讲过, 当一个 CPU 要读某个数据时, 会向其它 CPU 查询是否有该数据的缓存, 如果有, 拿过来, 如果没有, 去内存拿; 这个过程是锁总线的, 是串行的, 过程中所有 CPU 都不能使用总线;
-
当一个 CPU 要失效其它 CPU 中的数据时也是一样, 其它 CPU 要等待通知, 然后失效对应的缓存, 这个过程中不能去使用总线;
-
现在引入 SB 后, 当 CPU 要读取数据时, 由 SB 与其它 CPU 交互, 得到的结果暂存到 SB 中, CPU 此时可以去执行其它指令;
-
IQ 也是一样, 当有失效通知到来时, 先缓存到 IQ 中, CPU再异步地进行处理;
指令重排
指令重排通常出现在以下两个阶段:
编译器优化阶段:编译器在生成字节码或机器码时,为了提高执行效率,可能会对源代码中的指令进行重新排序。例如,编译器可能会将没有依赖关系的指令提前执行,以充分利用 CPU 的流水线能力。
处理器优化阶段:处理器为了最大化硬件资源的利用率,可能会在执行指令时重新调整指令的顺序。例如,在处理器的流水线中,如果某个指令的执行依赖于之前指令的结果,而该结果尚未准备好,处理器可能会先执行其他指令。
比如 SB 和 IQ 的引入, 就会导致修改不能立即可见以及指令重排的问题;
// 假设一开始 flag 值为true, 在线程 1 和线程 2都有缓存;
// 线程1先执行, 这将导致线程2的缓存失效, 但是因为invalidQueue, 线程2并不会立即收到这一信息;
{flag = false;
}// 线程二可能还没来得及处理IQ中的失效通知, 导致还是能通过 if 判断;
if(flag){// 导致还能进来;i++;
}// 明明我先把一个值改为 false 了, 其它线程却还是判断为 true, 这就发生了不可见;
// 本来应该 flag = false 然后 i++ 不执行, 现在却变成了相当于线程二先通过判断并执行 i++, 线程一再 flag = false
// 这就发生了指令重排;
指令重排问题举例: new对象
一次完整的 new 对象并执行构造方法的过程, 其字节码如下
new #2 <T>
dup
invokespecial #3 <T.<init>>
astore_1
return
-
new 分配空间, 并将该引用压到操作数栈; 分配以后所有成员都是默认值;
分配空间的时候有两种方式: 指针碰撞和空闲链表;
首先, 不考虑逃逸分析的话, 新对象的创建都在堆上;
Eden 区放得下, 就在 Eden 区分配; 如果是超大对象, 还有可能直接在老年代分配;
指针碰撞: 用一个指针指向当前空闲区域的起始位置;
适用于不会产生碎片的垃圾回收算法, 比如 Parallel Scavenge, 基于复制算法; 所以, 新生代上 new 对象, 一般适用指针碰撞;
空闲链表: 维护空闲链表, 每个元素对应一个空闲块; 适用于会产生碎片的垃圾回收算法; 比如CMS;
如何解决多线程同时分配内存的安全问题?
可以用 CAS;
可以用 TLAB; 每个线程初始化的时候, 分配一个 在分配内存权限上私有的 一块Buffer; 满了再申请; 分配是私有的, 访问不是;
-
dup 将栈顶的值复制一份再次入栈;
-
invokespecial 弹出栈顶, 作为 this 传给构造方法;
-
astore, 将弹出栈顶, 保存到当前方法的局部变量表中;
-
return, 返回;
-
由于指令重排, 有可能还没调用构造方法, 就放到局部变量表里了, 这时候去使用它, 用的是一个没有经过构造方法初始化的对象, 很危险;
指令重排问题举例: 单例模式
如何做一个线程安全的懒加载单例类? 大多数人的回答是DCL, 即 double check lock
private static singleton;
public static Singleton get(){// 外层的if 保证效率, 已经创建了单例对象的时候不会进入synchronized;if(singleton == null){synchronized(Singleton.class){// 内层保证多线程安全if(singleton == null)singleton = new Singleton();else{return singleton;}}}else{return singleton;}
}
-
正确的回答要在 DCL 的基础上, 给 singleton 引用加 volatile, 如果不加volatile, DCL也没用
private volatile static singleton;
-
因为new对象是个过程, 假设没有加volatile, 因为指令重排, 使得astore指令在 invokespecial 指令前执行; 那么线程一 new 对象 new 到一半, 所有成员还是默认值的情况下, 就把引用保存了, 这时如果线程2到来, 进行外层判断, singleton != null, 会直接把这个没有执行invokespecial的对象返回;
-
如果是一个初值为1000的账户, 那现在初始金额只有0;
volatile 如何解决可见性与有序性问题?
-
在源码中加volatile关键字, 编译为 class 文件后, 对应 ACC_VOLATILE 指令;
-
CPU 提供了内存屏障指令, 上层应用可以在合适的地方添加内存屏障指令来避免指令重排;
JVM 会自动对 volatile 变量的读写操作添加对应的内存屏障;
比如对 volatile 修饰的变量 x 进行写操作:
JVM 自动在写操作之前加 StoreStore 屏障, 表示前面的对普通变量的写操作完成, 当前的写操作才能执行;
后面加 StoreLoad, 表示当前的写操作执行完了, 后面对普通变量的读操作才能执行;
-
读写屏障在底层使用 lock 汇编指令, 通过对总线或者缓存行加锁的方式, 禁用 SB 和 IQ, 将对缓存的修改强制立即写入主存, 进而解决了可见性和有序性问题;
-
需要注意, volatile 并不保证原子性; 不过, 在一些场景下, 比如 CAS 操作一个变量, 通过 CAS 和 volatile 是可以同时解决三大问题的, 性能比synchronized 要好;
相关文章:

【Java 并发编程】(三) 从CPU缓存开始聊 volatile 底层原理
并发编程 三大问题 在并发编程中,原子性、有序性和可见性是三个重要的问题,解决这三个问题是保证多线程程序正确性的基础。原子性: 指的是一个操作不可分割, 要么全部执行完成, 要么不执行, 不存在执行一部分的情况.有序性: 有序性是指程序的执行顺序与…...

YOLOV8网络结构|搞懂Backbone-Conv
参数量计算: (输入通道*w)*(输出通道*w)*k^2+(输出通道*w)*2 w是模型缩放里面的width - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2 输出通道c2=64,k=3,s=2 P1/2 特征图变小一半 先定义算子层 再搭一个forward前向传播 class Conv(nn.Module):"""Standard convo…...
Elasticsearch Nested类型详解与实战
Elasticsearch(简称ES)是一个基于Lucene的全文搜索引擎,它提供了强大的搜索能力以及对数据的高效索引和查询。在ES中,数据通常以JSON格式存储,并且可以采用多种数据类型。其中,nested类型是一种特殊的对象数…...

网络编程,网络协议,UDP协议
网络: 1.协议:通信双方约定的一套标准 2.国际网络通信协议标准: 1.OSI协议: 应用层 发送的数据内容 表示层 数据是否加密 会话层 是否建立会话连接 传输层 …...
每日一题——第六十三题
题目:判断一个数是否为合数 #include <stdio.h> #include <stdbool.h> // 为了使用bool类型 // 函数声明,用于判断是否为合数 bool isComposite(int x); int main() { int x; printf("请输入一个正整数: "); scanf(&quo…...

人工智能算法,图像识别技术;基于大语言模型的跨境商品识别与问答系统;图像识别
目录 一 .研究背景 二,大语言模型介绍 三,数据采集与预处理 商品识别算法 四. 跨境商品问答系统设计 五.需要源码联系 一 .研究背景 在当今全球化的背景下,跨境电商行业迅速发展,为消费者提供了更广泛的购物选择和更便利的购物方式。然而…...
数据库系统 第18节 数据库安全
数据库安全是确保数据库管理系统(DBMS)中存储的数据的保密性、完整性和可用性的过程。以下是一些关键的数据库安全措施: 用户身份验证(Authentication): 这是确定用户或系统是否有权访问数据库的第一步。通…...

Golang | Leetcode Golang题解之第338题比特位计数
题目: 题解: func countBits(n int) []int {bits : make([]int, n1)for i : 1; i < n; i {bits[i] bits[i&(i-1)] 1}return bits }...
【Python变量简析】
Python变量简析 在 Python 中,变量是用于存储和操作数据的命名内存位置。变量的概念类似于代数中的方程变量,比如对于方程式 y x * x ,x 就是变量。 Python 变量具有以下特点: 变量名可以由字母、数字和下划线组成,…...

智慧零售模式下物流优化与开源AI智能名片S2B2C商城系统的深度融合
摘要:在数字化浪潮的推动下,智慧零售模式正逐步成为零售业的新常态。该模式通过深度融合物联网、大数据、人工智能等先进技术,实现了线上线下无缝衔接,为消费者提供了更加便捷、个性化的购物体验。物流作为智慧零售的重要支撑&…...
socket和websocket 有什么区别
Socket 和 WebSocket 都用于网络通信,但它们的用途、协议、以及使用方式有所不同。以下是两者的主要区别: ### 1. **基础协议** - **Socket**: - Socket 是网络通信的一个抽象概念,通常基于传输层协议,如 TCP(…...

亿玛科技:TiDB 6.1.5 升级到 7.5.1 经验分享
作者: foxchan 原文来源: https://tidb.net/blog/6e628afd 为什么要升级? 本次升级7.5的目的如下: 1、tidb有太多的分区表需要归档整理。7.5版本这个功能GA了。 2、之前集群tikv节点的region迁移过慢,影响tikv节…...

8.16-ansible的应用
ansible ansible是基于模块工作的,本身没有批量部署的能力。真正具有批量部署的是ansible所运行的模块,ansible只是提供一种框架。 格式 ansible 主机ip|域名|组名|别名 -m ping|copy|... 参数 1.ping模块 m0 # 查看有没有安装epel [rootm0 ~]#…...
相似度计算方法-编辑距离 (Edit Distance)
定义 编辑距离(Edit Distance),也称为Levenshtein距离,是一种衡量两个字符串相似度的方法。它定义为从一个字符串转换为另一个字符串所需的最少单字符编辑操作次数,这些操作包括插入、删除或替换一个字符。 计算方法 …...
初识FPGA
大学的时候有一门verilog语言,觉得很难,不愿学。有学习套件是黑金的一块FPGA开发板,可能当时点灯和点数码管了。全都忘了。 今项目需要,使用FPGA中的ZYNQ,需要c语言开发,随即开始学习相关知识。 ZYNQ内部…...
探索 JavaScript:从入门到精通
目录 1. JavaScript 的介绍与基础 示例:弹出欢迎信息 JavaScript,作为网络时代最流行的脚本语言之一,赋予了网页生动活泼的动态功能。无论是新手还是经验丰富的开发者,掌握 JavaScript 的核心概念和技能都是开启网络编程之门的钥…...

这4款视频压缩软件堪称是压缩界的神器!
视频在我们的日常设备当中会占用相对较多的空间,尤其是喜欢用视频记录的朋友。但是过多过大的视频不仅会给我们的设备带来了压力,也不利于分享和管理。今天我就要给大家分享几个视频压缩的小妙招。 1、福昕压缩 直通车:www.foxitsoftware.cn…...
【ARM 芯片 安全与攻击 5.6 -- 侧信道与隐蔽信道的区别】
文章目录 侧信道与隐蔽信道的区别侧信道攻击(Side-channel Attack)侧信道攻击简介侧信道攻击 使用方法侧信道攻击示例隐蔽信道(Covert Channel)隐蔽信道简介隐蔽信道使用方法隐蔽信道代码示例侧信道与隐蔽信道在芯片及系统安全方面的使用侧信道的应用隐蔽信道的应用Summary…...
C#:Bitmap类使用方法—第4讲
大家好,今天接着上一篇文章继续讲。 下面是今天的方法: (1)Bitmap.MakeTransparent 方法:使此 Bitmap的默认透明颜色透明。 private void MakeTransparent_Example1(PaintEventArgs e) { // Create a Bitmap object…...
Vue是如何实现nextTick的?
你好同学,我是沐爸,欢迎点赞、收藏和关注。个人知乎 Vue.js 的 nextTick 函数是一个非常重要的功能,它用于延迟执行代码块到下次 DOM 更新循环之后。这在 Vue.js 的异步更新队列机制中非常有用,尤其是在你需要基于更新后的 DOM 来…...

React第五十七节 Router中RouterProvider使用详解及注意事项
前言 在 React Router v6.4 中,RouterProvider 是一个核心组件,用于提供基于数据路由(data routers)的新型路由方案。 它替代了传统的 <BrowserRouter>,支持更强大的数据加载和操作功能(如 loader 和…...

智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南 在数字化营销时代,邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天,我们将深入解析邮件打开率、网站可用性、页面参与时…...

GitFlow 工作模式(详解)
今天再学项目的过程中遇到使用gitflow模式管理代码,因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存,无论是github还是gittee,都是一种基于git去保存代码的形式,这样保存代码…...

华为OD机考-机房布局
import java.util.*;public class DemoTest5 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseSystem.out.println(solve(in.nextLine()));}}priv…...

计算机基础知识解析:从应用到架构的全面拆解
目录 前言 1、 计算机的应用领域:无处不在的数字助手 2、 计算机的进化史:从算盘到量子计算 3、计算机的分类:不止 “台式机和笔记本” 4、计算机的组件:硬件与软件的协同 4.1 硬件:五大核心部件 4.2 软件&#…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

R 语言科研绘图第 55 期 --- 网络图-聚类
在发表科研论文的过程中,科研绘图是必不可少的,一张好看的图形会是文章很大的加分项。 为了便于使用,本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中,获取方式: R 语言科研绘图模板 --- sciRplothttps://mp.…...

系统掌握PyTorch:图解张量、Autograd、DataLoader、nn.Module与实战模型
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文通过代码驱动的方式,系统讲解PyTorch核心概念和实战技巧,涵盖张量操作、自动微分、数据加载、模型构建和训练全流程&#…...