高并发之volatile、synchronized关键和内存屏障(Memory Barrier)

简介: 高并发之volatile、synchronized关键和内存屏障(Memory Barrier)
                            ✨ 我是喜欢分享知识、喜欢写博客的YuShiwen,与大家一起学习,共同成长!
                                      📢 闻到有先后,学到了就是自己的,大家加油!
📢 导读:
本期总共有4个章节,
⛳️ 第一个章节是让大家了解电脑时钟脉冲,它是什么,有什么作用。
⛳️ 第二个章是介绍为什么要引入缓存以及他的物理结构图。
⛳️ 第三个章节是与第二章节环环相扣,在3.1章节中首先抛出缓存存在的问题,然后在3.2章节中来引入Store Buffer解决3.1章节中的问题,然后又引入Invalidate Queue对3.2章节进行了优化,并且详细介绍了它们的原理还给出了结构图。
⛳️ 第四个章节是关于内存屏障的讲解,这个章节是全文的重点。
文章的整体内容层层递,展现了之前的大佬们分析问题和解决问题的思路,希望大家可以耐心看完,保证会有所收获,该篇文章笔者花费2.5天时间创作完成,质量不会差,大家加油!
                                

⛳️1.了解时钟脉冲

在了解内存屏障之前,首先我们得知道电脑中时钟脉冲的作用,如下:

电脑通过使用时钟来同步执行指令,时钟脉冲的频率(称为时钟频率)基本上是固定的。在电脑中它就是由一个频率相当精确和稳定的脉冲信号发生器发出的。脉冲信号通常是由晶体通电振荡产生的。同样内存缓存也有自己的时许,比如内存时序(英语:Memory timings或RAM timings)是描述同步动态随机存取存储器(SDRAM)性能的四个参数:CL、TRCD、TRP和TRAS,单位为时钟周期。当你买了一台1.5GHz的电脑,1.5GHz就是时钟频率,即每秒15亿次的时钟脉冲。时钟并不记录分和秒。它以不变的速率简单跳动。电子计算机通过使用这个跳动来正确执行它们的操作,就像节拍器的跳动如何来帮助你以正确的节奏播放音乐。一个指令需要跳动的次数(或就像他们经常说的执行周期)依赖CPU的产生和模仿。周期的次数取决于它之前的指令和其他因素。总的来说,时钟脉冲他可以统一协调各部件的工作,算是一个统一的节拍。

⛳️2.引入缓存

我们都知道对于i++操作:(在不考虑缓存的情况下,下面我将用这个例子引出缓存)

读内存,也就是加载操作(load), 从内存读到寄存器(关于CPU与寄存器可参考本人该篇博文:CPU和寄存器详解)在通用寄存器中将i的值自增1写内存,就就是存储操作(store),从寄存器写入到内存

如果直接使用内存与CPU中的寄存器交互,会存在如下问题:

我们知道,CPU和内存之间存在速度差异。程序的指令,如果需要从CPU Register里面取数据,CPU只需要0 cycles(CPU周期,即上述提到的一次脉冲); 如果需要直接访问内存,则需要几百个cycles。(另外,如果能从高速缓存取到目标数据,需要4-75个cycles);一个非常之快的CPU(处理器),却要和一个更慢的RAM(内存)交换数据,自己也会从一个cycles变成几百个cycles。毫无疑问,如果直接交换数据,那么CPU会因为RAM太慢,拖慢自己的速度。CPU访问个存储位置所需的时间如下:
CPU访问大约需要的周期(cycle)大约需要的时间寄存器1 cycle0nsL1 Cache3—4 cycle1nsL2 Cache10—20 cycle3nsL3 Cache40—45 cycle15ns内存60—90ns

那么如何解决这个问题呢?引入三级缓存:

为了解决两者之间数据交换数据的速度差异,计算机科学家们了就引入了高速缓存(Cache):让CPU和RAM之间使用多层级的缓存,避免两者直接交换数据,而是从临近的缓存区交换数据。越靠近CPU寄存器的缓存速度越快,造价越越贵,下图的三级缓存与寄存器之间的结构图:(其中的Thread0、Thread1可以理解成CPU0的寄存器、CPU1的寄存器)
在这里插入图片描述
上图可以简化为下图:

在这里插入图片描述

小插曲,缓存行的概念:

为了简化与RAM之间的通信,高速缓存控制器是针对数据块,而不是字节进行操作的。从程序设计的角度讲,高速缓存其实就是一组称之为缓存行(cache line)的固定大小的数据块,目前一般为64Byte。关于缓存行的深入理解,大家可以看下笔者的这篇文章:
高并发之伪共享和缓存行填充(缓存行对齐)(@Contended)

⛳️3.引入Store Buffer

3.1.缓存存在的问题:

如果CPU0发起一次对某个地址的写操作,但是其本地缓存中没有数据,这个数据存放在CPU1的本地缓存中。那么此时会进行如下操作:(缓存一致性协议MESI,具体的内容见笔者该篇文章:“了解高并发底层原理”,面试官:讲一下MESI(缓存一致性协议)吧)
根据MESI协议,在CPU缓存之间会进行如下沟通:

为了完成这次操作,CPU0会发出一个invalidate的信号,使其他CPU的cache数据无效(因为CPU0需要重新写这个地址中的值,说明这个地址中的值将被改变,如果不把其他CPU中存放的该地址的值无效,那么就有可能会出现数据不一致的问题)。CPU0的invalidate信号后,会等待其他CPU对于该信号的回复,即其他CPU需要回复invalidate acknowledge信号(消息)来告知CPU0我们已经接收到了invalidate信号,把cache数据变为无效的了。而这个数据可能不只在CPU1的缓存中,还可能在CPU2、CPU3的缓存中,在所有CPU都回复给CPU0信号后才能真正发起写操作。这个需要等待非常长的时间,这就导致了性能上的损耗。

如何解决上面的这个问题呢?这个时候我们引入Store Buffer。

3.2 引入Store Buffer来解决3.1中的问题

加入了这个Store Buffer存储缓存区硬件结构后:
此时CPU0需要往某个地址中写入一个数据时:

它不需要去关心其他的CPU的local cache中有没有这个地址的数据,它只需要把它需要写的值直接存放到store buffer中,然后发出invalidate的信号。等到其他CPU回复invalidate的信号后,再把CPU0存放在store buffer中的数据推到CPU0的本地缓存中。这样就避免了CPU0等待其他CPU的响应了。

ps:每一个CPU core都拥有自己私有的store buffer,一个CPU只能访问自己私有的那个store buffer。

3.3 引入Invalidate Queue对3.2的进一步优化

store buffer的大小是有限的,所有的写入操作发生cache missing(数据不再本地)都会使用store buffer,因此store buffer很容易会满;当store buffer满了之后,需要写如数据的cpu还是会等待其他的CPU响应Invalidate信号以处理store buffer中的数据,即把store buffer中的数据推到CPU的本地缓存中。因此还是要回到其他CPU响应Invalidate信号上面来,其他CPU回复invalidate acknowledge信号(消息)来告知CPU0我们已经接收到了invalidate信号,把cache line数据变为无效的了。如果一个CPU很忙,可能导致需要回复信号的cpu无法按时回复invalidate acknowledge信号(消息),这就可能会导致写入数据的cpu在等它回Invalidate ACK。解决思路还是化同步为异步: cpu不必要处理了cache line之后才回Invalidate ACK,而是可以先将Invalid消息放到某个请求队列Invalid Queue,然后就返回Invalidate ACK。CPU可以后续再处理Invalid Queue中的消息,大幅度降低Invalidate ACK响应时间。

如下图:
在这里插入图片描述

⛳️4.内存屏障

说到这里,内存屏障它终于来了。

4.1volatile和synchronized关键字与内存屏障的关系:

我们都知道在Java中,如果不使用volatile和synchronized指令可能会发生重排,指令重排分为编译器指令重排和CPU指令重排。Java多线程程序通常使用高层程序设计语言中的同步原语,比如volatile和synchronized,因此一般不需要明确使用内存屏障。也就是说在Java中我们使用的是volatile和synchronized关键字,javac编译转化成字节码的时候,还是用到了内存屏障。

4.2内存屏障能解决哪些问题:

可见性
内存可见性问题,主要是高速缓存与内存的一致性问题。一个处理器上的线程修改了某数据,而在另一处理器上的线程可能仍然使用着该数据在专用cache中的老值,这就是可见性出了问题。禁止重排
编译器和CPU为了提高代码的执行效率,可能会对代码进行重新排序。

4.3底层的内存屏障是什么:

大多数处理器提供了内存屏障指令:

完全内存屏障(full memory barrier)确保内存读和写操作;保障了内存屏障前的读写操作执行完毕、并且将结果提交到内存之后,再执行晚于屏障的读写操作。内存读屏障(read memory barrier)仅确保了内存读操作;保障了内存屏障前的操作执行完毕、并且将结果提交到内存之后,再执行晚于屏障的操作。内存写屏障(write memory barrier)仅保证了内存写操作。保障了内存屏障前的操作执行完毕、并且将结果提交到内存之后,再执行晚于屏障的操作。

内存屏障是底层原语,是内存排序的一部分,在不同体系结构下变化很大而不适合推广。需要认真研读硬件的手册以确定内存屏障的办法。

4.4Java中的内存屏障

上面提到了内存屏障可简单分为读屏障和写屏障。这两种的组合就有如下这几种情况:
read read ,write write ,read write ,write read实际上也是上述两种的组合,完成一系列的屏障和数据同步功能:

`read_read屏障:对于这样的语句read1语句;read_read屏障; read2语句,在read2及后续读取操作要读取的数据被访问前,保证read1要读取的数据被读取完毕。write_write屏障:对于这样的语句write1语句; write_write屏障; write2语句,在write2及后续写入操作执行前,保证write1的写入操作对其它处理器可见。read_write屏障:对于这样的语句read语句; read_write屏障;write语句,在write及后续写入操作被刷出前,保证read要读取的数据被读取完毕。write_read屏障:对于这样的语句write语句; write_read屏障; read语句,在read及后续所有读取操作执行前,保证write的写入对所有处理器可见。

这里拿volatile举例,volatile它非常的悲观且严格,volatile的具体屏障如下:

volatile long i = 1; 

对于写:

在每个volatile写操作前插入write_write屏障,在写操作后插入write_read屏障;也就是 语句 write语句; write_write屏障 ;long i = 1语句 ; write_read屏障;read语句。

对于读:

在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;也就是 语句 read语句; LoadLoad屏障 ;long i = 1语句 ; LoadStore屏障;write语句。
目录
相关文章
|
3月前
|
存储 SQL 缓存
揭秘Java并发核心:深度剖析Java内存模型(JMM)与Volatile关键字的魔法底层,让你的多线程应用无懈可击
【8月更文挑战第4天】Java内存模型(JMM)是Java并发的核心,定义了多线程环境中变量的访问规则,确保原子性、可见性和有序性。JMM区分了主内存与工作内存,以提高性能但可能引入可见性问题。Volatile关键字确保变量的可见性和有序性,其作用于读写操作中插入内存屏障,避免缓存一致性问题。例如,在DCL单例模式中使用Volatile确保实例化过程的可见性。Volatile依赖内存屏障和缓存一致性协议,但不保证原子性,需与其他同步机制配合使用以构建安全的并发程序。
69 0
|
12天前
|
监控 Java 数据库连接
线程池在高并发下如何防止内存泄漏?
线程池在高并发下如何防止内存泄漏?
|
15天前
|
Rust 编译器
|
2月前
|
存储 网络协议 大数据
一文读懂RDMA: Remote Direct Memory Access(远程直接内存访问)
该文档详细介绍了RDMA(远程直接内存访问)技术的基本原理、主要特点及其编程接口。RDMA通过硬件直接在应用程序间搬移数据,绕过操作系统协议栈,显著提升网络通信效率,尤其适用于高性能计算和大数据处理等场景。文档还提供了RDMA编程接口的概述及示例代码,帮助开发者更好地理解和应用这一技术。
|
3月前
|
存储 缓存 NoSQL
Redis内存管理揭秘:掌握淘汰策略,让你的数据库在高并发下也能游刃有余,守护业务稳定运行!
【8月更文挑战第22天】Redis的内存淘汰策略管理内存使用,防止溢出。主要包括:noeviction(拒绝新写入)、LRU/LFU(淘汰最少使用/最不常用数据)、RANDOM(随机淘汰)及TTL(淘汰接近过期数据)。策略选择需依据应用场景、数据特性和性能需求。可通过Redis命令行工具或配置文件进行设置。
71 2
|
3月前
|
设计模式 uml
在电脑主机(MainFrame)中只需要按下主机的开机按钮(on()),即可调用其它硬件设备和软件的启动方法,如内存(Memory)的自检(check())、CPU的运行(run())、硬盘(Hard
该博客文章通过一个电脑主机启动的示例代码,展示了外观模式(Facade Pattern)的设计模式,其中主机(MainFrame)类通过调用内部硬件组件(如内存、CPU、硬盘)和操作系统的启动方法来实现开机流程,同时讨论了外观模式的优缺点。
|
3月前
|
存储 缓存 监控
托管内存(Managed Memory)
托管内存(Managed Memory)
|
4月前
|
监控 安全 Java
JVM内存问题之排查Direct Memory泄漏有哪些常用方法
JVM内存问题之排查Direct Memory泄漏有哪些常用方法
114 2
|
4月前
|
Arthas 监控 Java
JVM内存问题之使用gperftools分析JNI Memory泄漏的具体步骤是什么
JVM内存问题之使用gperftools分析JNI Memory泄漏的具体步骤是什么
|
4月前
|
存储 缓存 Java
(一) 玩命死磕Java内存模型(JMM)与 Volatile关键字底层原理
文章的阐述思路为:先阐述`JVM`内存模型、硬件与`OS`(操作系统)内存区域架构、`Java`多线程原理以及`Java`内存模型`JMM`之间的关联关系后,再对`Java`内存模型进行进一步剖析,毕竟许多小伙伴很容易将`Java`内存模型(`JMM`)和`JVM`内存模型的概念相互混淆,本文的目的就是帮助各位彻底理解`JMM`内存模型。
107 0