【并发】深入理解JMM并发三大特性(二)

news2025/4/8 1:02:07

t【并发】深入理解JMM&并发三大特性(二)

我们在上一篇文章中提到了JMM内存模型,并发的三大特性,其中对可见性做了详细的讲解!

这一篇文章,将会站在硬件层面继续深入讲解并发的相关问题! 

【并发】深入理解JMM&并发三大特性(一)_面向架构编程的博客-CSDN博客https://blog.csdn.net/weixin_43715214/article/details/127895883

一、JMM可见性问题回顾

我们上一篇文章,主要讲了 volatile 的作用

volatile 是为了解决,当前线程对共享变量的操作会存在“读不到”,或者不能立即读到另一个线程对此共享变量的“写操作”(可能需要隔一段时间才可以读到)的问题!

我们解决可见性问题,上一篇文章提到了很多种解决方案,最常用的、最优解一般都是采用volatile修饰共享变量!锁机制、用final修饰也可以达到一样的效果,但是会有一定的性能问题和局限性。(当然,如果业务中“必须”要用到锁,那么改共享变量自然也不用多此一举去加一个volatile了!)

疑问】如果业务逻辑超过1ms,是不是就不需要volatile???

之前在程序中会去调一个我们自己写的shortWait()函数,用于模拟业务逻辑的执行时间。

public  class VisibilityTest {

    private boolean flag = true;
    private int count = 0;

    public void refresh() {
        flag = false;
        System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
    }

    public void load() {
        System.out.println(Thread.currentThread().getName() + "开始执行.....");
        while (flag) {
            // TODO 业务逻辑
            shortWait(1000000);
            count++;
        }
        System.out.println(Thread.currentThread().getName() + "跳出循环: count=" + count);
    }

    public static void main(String[] args) throws InterruptedException {
        VisibilityTest test = new VisibilityTest();

        Thread threadA = new Thread(() -> test.load(), "threadA");
        threadA.start();

        Thread.sleep(1000);
        Thread threadB = new Thread(() -> test.refresh(), "threadB");
        threadB.start();

    }

    // 模拟业务执行时间
    public static void shortWait(long interval) {
        long start = System.nanoTime();
        long end;
        do {
            end = System.nanoTime();
        } while (start + interval >= end);
    }
}

所以,是不是可以说,如果我们知道这一块的业务比较复杂,要查数据库等等,操作较多,一定会超过1ms那么是不是可以不用volatile

直接说结论:不行!!!

注意!这里是一个天坑!!! 

先看下面的程序,我们将之前的调shortWait(),改为直接在业务逻辑部分编写,但是运行结果居然诡异的发生了变化!!! 

public  class VisibilityTest {

    private boolean flag = true;
    private int count = 0;

    public void refresh() {
        flag = false;
        System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
    }

    public void load() {
        System.out.println(Thread.currentThread().getName() + "开始执行.....");
        while (flag) {
            // TODO 业务逻辑

            // do-while的逻辑 shortWait()
            long start = System.nanoTime();
            long end;
            do {
                end = System.nanoTime();
            } while (start + 1000000 >= end);
            count++;
        }
        System.out.println(Thread.currentThread().getName() + "跳出循环: count=" + count);
    }

    public static void main(String[] args) throws InterruptedException {
        VisibilityTest test = new VisibilityTest();

        Thread threadA = new Thread(() -> test.load(), "threadA");
        threadA.start();

        Thread.sleep(1000);
        Thread threadB = new Thread(() -> test.refresh(), "threadB");
        threadB.start();
    }
}

但是,假如我们把do-while改为while,那么运行结果居然又奇迹般的发生了变化

long start = System.nanoTime();
long end = 0;
while (start + 1000000 >= end) {
    end = System.nanoTime();
}

为什么while可以,do-while不行???小编也不清楚,听完图灵的老师讲课他也说不清楚。

所以,像这种令人匪夷所思的场景肯定还有,还不如直接在变量上加一个volatile来的保险!

二、多CPU多核缓存架构解析

1. CPU高速缓存(Cache Memory)

CPU缓存高速缓冲存储器,是位于CPU主内存间的一种容量较小但速度很高的存储器。它是在寄存器内存之间的“东西”。

如下图所示: 

由于CPU的速度远高于主内存,CPU直接从内存中存取数据要等待一定时间周期,Cache中保存着CPU刚用过或循环使用的一部分数据,当CPU再次使用该部分数据时可从Cache中直接调用减少CPU的等待时间,提高了系统的效率

在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就是局部性原理

时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。

比如循环、递归、方法的反复调用等。

空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。

比如顺序执行的代码、连续创建的两个对象、数组等。

2. 多CPU多核缓存架构 

物理CPU:物理CPU就是插在主机上的真实的CPU硬件

核心数:我们常常会听说多核处理器,其中的核指的就是核心数

逻辑CPU:逻辑CPU跟超线程技术有联系,假如物理CPU不支持超线程的,那么逻辑CPU的数量等于核心数的数量;如果物理CPU支持超线程,那么逻辑CPU的数目是核心数数目的两倍

现代CPU为了提升执行效率,减少CPU与内存的交互,一般在CPU上集成了多级缓存架构,常见的为三级缓存结构。

每个核都有一个独享L1Cache(L1Cache分为数据缓存指令缓存)和L2Cache;而L3Cache是所有的核共享缓存。 

  • 一级缓存

都内置在CPU内部并与CPU同速运行,可以有效的提高CPU的运行效率。一级缓存越大,CPU的运行效率越高,但受到CPU内部结构的限制,一级缓存的容量都很小。

  • 二级缓存

它是为了协调一级缓存内存之间的速度。cpu调用缓存首先是一级缓存,当处理器的速度逐渐提升,会导致一级缓存就供不应求,这样就得提升到二级缓存了。二级缓存它比一级缓存的速度相对来说会慢,但是它比一级缓存的空间容量要大。主要就是做一级缓存内存之间数据临时交换的地方用

  • 三级缓存

是为读取二级缓存后未命中的数据设计的—种缓存,在拥有三级缓存的CPU中,只有约5%的数据需要从内存中调用,这进一步提高了CPU的效率。其运作原理在于使用较快速的储存装置保留一份从慢速储存装置中所读取数据并进行拷贝,当有需要再从较慢的储存体中读写数据时,缓存(cache)能够使得读写的动作先在快速的装置上完成,如此会使系统的响应较为快速。

可以参考这篇文章-三级缓存: 

初步理解三级缓存Cache__古_凡_的博客-CSDN博客_三级cachehttps://blog.csdn.net/Ang_ie/article/details/115335431三级缓存与CPU寄存器、内存的关系示意图:

由于现在的CPU都是多核缓存,那么对于同一个变量的运算很可能发生在不同的核心中!那么势必会出现缓存不一致的问题!!!

我们现在要研究的就是CPU是通过什么方式、什么机制来保证缓存一致的???

三、缓存一致性问题与Bus snooping机制(总线窥探)详解

10 张图打开 CPU 缓存一致性的大门 - 小林coding - 博客园 (cnblogs.com)https://www.cnblogs.com/xiaolincoding/p/13886559.html

1. 缓存一致性(Cache coherence)

计算机体系结构中,缓存一致性共享资源数据一致性,这些数据最终存储在多个本地缓存中。当系统中的客户机维护公共内存资源的缓存时,可能会出现数据不一致的问题,这在多处理系统中的CPU中尤其如此。

共享内存多处理器系统中,每个处理器都有一个单独的缓存内存,共享数据可能有多个副本:一个副本在主内存中,一个副本在请求它的每个处理器的本地缓存中。当数据的一个副本发生更改时,其他副本必须反映该更改。

缓存一致性是确保共享操作数(数据)值的变化能够及时地在整个系统中传播的机制。

缓存一致性的要求(前提)

(1)写传播(Write Propagation)

某个 CPU 核心里的 Cache 数据更新时,必须要传播其他核心的 Cache。

(2)事务串行化(Transaction Serialization)

某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的。

如何保障一致性?

确保一致性的两种最常见的机制是窥探机制(snooping )基于目录的机制(directory-based)。如果处理器的核心数大于64,那么一般采用目录机制,否则都是窥探机制

2. 总线窥探(Bus Snooping)

原理 

当特定数据被多个缓存共享时,处理器修改了共享数据的值,更改必须传播到所有其他具有该数据副本的缓存中。这种更改传播可以防止系统违反缓存一致性。数据变更的通知可以通过总线窥探来完成。

所有的窥探者都在监视总线上的每一个事务。如果一个修改共享缓存块的事务出现在总线上,所有的窥探者都会检查他们的缓存是否有共享块的相同副本

如果缓存中有共享块的副本,则相应的窥探者会执行“一个动作”以确保缓存一致性。这个动作可以是刷新缓存块使缓存块失效。它还涉及到缓存块状态的改变,这取决于缓存一致性协议(cache coherence protocol)。

窥探协议类型

根据管理写操作的本地副本的方式,有两种窥探协议:

(1)Write-invalidate 写失效协议

当处理器写入一个共享缓存块时,其他缓存中的所有共享副本都会通过总线窥探失效。这种方法确保处理器只能读写一个数据的一个副本。其他缓存中的所有其他副本都无效。这是最常用的窥探协议。MSI、MESI、MOSI、MOESI和MESIF协议属于该类型。

(2)Write-update 写更新协议(有性能问题)

当处理器写入一个共享缓存块时,其他缓存的所有共享副本都会通过总线窥探更新。这个方法将写数据广播到总线上的所有缓存中。它比write-invalidate协议引起更大的总线流量。这就是为什么这种方法不常见。Dragon和firefly协议属于此类别。

MESI协议

MESI协议是一个基于写失效的缓存一致性协议,是支持回写(write-back)缓存的最常用协议。也称作伊利诺伊协议 (Illinois protocol,因为是在伊利诺伊大学厄巴纳-香槟分校被发明的)。与写通过(write through)缓存相比,回写缓冲能节约大量带宽。总是有“脏”(dirty)状态表示缓存中的数据与主存中不同。MESI协议要求在缓存不命中(miss)且数据块在另一个缓存时,允许缓存到缓存的数据复制。与MSI协议相比,MESI协议减少了主存的事务数量。这极大改善了性能。

它有4中状态:

已修改Modified (M)

缓存行是脏的(dirty),与主存的值不同。如果别的CPU内核要读主存这块数据,该缓存行必须回写到主存,状态变为共享(S).

独占Exclusive (E)

缓存行只在当前缓存中,但是干净的--缓存数据同于主存数据。当别的缓存读取它时,状态变为共享;当前写数据时,变为已修改状态。

共享Shared (S)

缓存行也存在于其它缓存中且是未修改的。缓存行可以在任意时刻抛弃。

无效Invalid (I)

缓存行是无效的

四、有序性与volatile禁止重排序场景分析

五、JVM内存屏障

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

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

相关文章

将整数字符串转成整数值

题目: 给定一个字符串 str,如果str符合日常书写的整数形式,并且属于 32 位整数的范围,返回 str 所代表的整数值,否则返回 0 。 举例: str "123" 返回 123 str "023" 返回 23 …

springboot整合之统一异常处理

特别说明:本次项目整合基于idea进行的,如果使用Eclipse可能操作会略有不同,不过总的来说不影响。 springboot整合之如何选择版本及项目搭建 springboot整合之版本号统一管理 springboot整合mybatis-plusdurid数据库连接池 springboot整合…

FFmpeg简单使用:过滤器 ---- h264_mp4toannexb

H264有两种封装方式:字节流AnnexB格式 AVCC格式。 1. AnnexB格式 ---- 用于实时播放 开始前缀(00000001或000001)+NALU数据  绝大部分编码器的默认输出格式   一共有两种起始码start_code    ①3字节0x000001  单帧多s…

C++面向对象特性——多态

C面向对象之多态什么是多态?为什么使用多态?虚函数的定义虚函数的实现机制哪些函数不能被设置为虚函数?虚函数的访问指针访问引用访问对象访问成员函数中的访问构造函数和析构函数中访问纯虚函数抽象类虚析构函数重载、隐藏、覆盖菱形继承虚拟…

spring boot文档阅读笔记——01

目录标题一、文档地址二、第一个spring boot例子三、 Starters(spring boot 官方提供的启动器)四、SpringBootApplication注释(一)EnableAutoConfiguration(二)ComponentScan五、devtools(热插拔…

当项目经理看世界杯决赛时…

12月18日,2022卡塔尔世界杯决赛,阿根廷在点球大战中击败卫冕冠军的法国队,捧走大力神杯。这场跌宕起伏的“巅峰对决”,给大家呈现了一场精彩绝伦的比赛。 当阿根廷2-0领先七十多分钟的时候,都以为这局稳了&#xff0c…

跨平台应用开发进阶(五十一):HTML5(富文本内容)连续数字、字母不自动换行问题分析及解决

文章目录一、前言二、问题分析三、解决方法3.1 对 input 标签设置3.2 对 input 标签内的 p 标签设置四、延伸阅读 顶部状态栏穿透问题五、拓展阅读一、前言 项目开发过程中,涉及在Web端维护富文本内容,通过APP端查看的相关的功能,功能描述大…

repo init详解

首先选择manifest源,一般直接使用清华或中科大的镜像源 repo init -u https://aosp.tuna.tsinghua.edu.cn/platform/manifest是清华提供的镜像源 repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest是中国科学技术大学的镜像源 repo init推荐使用-b 分…

未来汽车产业新生态高峰论坛在深圳举行

【2022年12月22日发自深圳】汽车产业正面临百年未有之大变局,以数字化技术为特征的智能网联汽车已经成为全球汽车产业转型升级的战略方向。汽车的属性也从一个机械化的交通工具转变成与各个生态相互连通的移动终端和数字空间。12月21日,由工业和信息化部…

【C++初阶】模板进阶

文章目录非类型模板参数模板特化函数模板特化类模板特化全特化偏特化模板的分离编译模板总结所有测试的代码非类型模板参数 模板参数分类类型形参与非类型形参 1.类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。 2.非类型形…

创建城市人口总量趋势图

创建城市人口总量趋势图学习目标使用数据人口总量趋势方程数据导入对数据进行处理fct_reorder2的应用对数据进行整理对图像进行可视化操作使用fct_reorder2去掉趋势点内容小结学习目标 我们所采用的学习内容来自B站的Lizongzhang老师的R语言的学习分享 今天学习的主要内容是关…

数据集说明:COCO的两个数据集COCO-stuff和COCO-Caption

数据集图片数量比较 COCO-Caption有33万张图、50万个caption COCO-Stuff有16.4万张图 172个类别(80thing、91stuff、1个未标注类) COCO-stuff的具体类别说明 COCO-Stuff contains 172 classes:80 thing, 91 stuff, and 1 class unlabeled. The 80 thing classes are the same …

群晖nas部署python项目

事件起因是因为想再硬盘里下载小姐姐,在线播放会卡顿很不爽, 开始用一些在线爬的网站,网速非常慢,我发现自己有nas 如果把项目部署再nas上 通过命令行下载就好了 我就开始检索资料,发现网上的都是一些过时的文章&#…

分布式系统关键路径延迟分析实践

作者 | 月色如海 导读 随着对用户体验的不断追求,延迟分析成为大型分布式系统中不可或缺的一环。本文介绍了目前在线服务中常用的延迟分析方法,重点讲解了关键路径分析的原理和技术实现方案,实践表明此方案效果显著,在耗时优化方面…

图解深度学习-提高泛化能力的方法

数据集 在深度学习的训练过程中,神经网络的类型和结构固然重要,但训练样本才是重中之重。 数据增强 当训练样本数量较少时,可以通过数据增强来增加样本的多样性。 数据增强就是通过对样本图像进行平移、旋转、镜像翻转等方式进行变换。除…

【Java】Log4j日志

目录 1.日志概述 日志是什么 为什么会有日志 2.log4j概述 什么是log4j 版本 3.log4j入门案例 步骤 实现 总结 4.log4j1详情:记录器rootLogger 5.log4j1详情:日志级别 6.log4j1详情:输出源appender ConsoleAppender FileAppend…

第二十八章 数论——扩展欧几里德算法与线性同余方程

第二十八章 扩展欧几里德算法一、裴蜀定理1、定理内容2、定理证明二、扩展欧几里德定理1、作用2、思路3、代码三、线性同余方程1、问题2、思路3、代码一、裴蜀定理 1、定理内容 对于任意整数aaa和bbb,一定存在整数xxx,yyy使得axbyaxbyaxby是gcd(a,b)gc…

Linux的基本指令

前言 相对于linux操作系统,其实window也是操作系统,我们这节课说的是Linux的基本指令 那么在window上有没有基本指令呢,答案是有的,我们可以使用windowsr进入cmd就可以用我们的基本指令 基本指令 在本文中,我们基本上都使用类比的…

基于threeJS实现圣诞节孔明灯效果

1.效果图 2.实现思路 使用three.js的套路几乎是固定的: 1 初始化场景(scene) 2.创建透视相机(camera) 3.设置相机位置(position) 4.创建纹理加载器对象(texture) 5.创建…

【SegNeXt】语义分割中对卷积注意力设计的反思

目录 1.摘要 2.相关工作 2.1语义分割 2.2多尺度网络 2.3注意力机制 3.网络结构 3.1卷积编码器 3.2解码器 4.实验 个人总结: 论文链接:论文 代码链接:代码 论文发表于NeurIPS 2022,值得注意的是,在Transforme…