二:深入理解 JAVA 内存模型 JMM

news/2024/5/2 19:06:14/文章来源:https://blog.csdn.net/sco5282/article/details/137506688

目录

  • 1、为什么要有内存模型
    • 1.1、为什么要有多级缓存?
    • 1.2、缓存一致性问题
    • 1.3、处理器优化和指令重排
  • 2、并发编程的三大问题
    • 2.1、原子性问题
    • 2.2、有序性问题
    • 2.3、可见性问题
    • 2.4、三大特性
  • 3、什么是内存模型?
    • 3.1、概念
    • 3.2、内存模型到底是怎么保证缓存一致性的呢?
    • 3.3、缓存一致性协议 —— MESI 协议
  • 4、什么是 Java 内存模型?
    • 4.1、概念
    • 4.2、实现
      • 4.2.1、原子性
      • 4.2.2、可见性
      • 4.2.3、有序性

1、为什么要有内存模型

1.1、为什么要有多级缓存?

计算机在执行程序时,每条指令都是在 CPU 中执行的,而执行的时候,免不了和数据打交道。

在早期时,数据是存储在内存中。但是随着 CPU 技术的发展,CPU 的执行速度越来越快。而内存技术并没有太大的变化。所以,从内存中读取/写入数据的过程和 CPU 的执行速度比起来差距就会越来越大,这就导致 CPU 每次操作内存都要耗费很多等待时间

但是,不能因为内存的读写速度慢,就不发展 CPU 技术,不能让内存成为计算机处理的瓶颈。

所以,人们想到了一个办法:在 CPU 和内存之间增加高速缓存(特点是:速度快、存储空间小、价格昂贵)

程序执行过程就变成了:

当程序在运行过程中,会将运算需要的数据从内存复制一份到 CPU 的高速缓存中。那么,当CPU 进行计算时,就可以直接从它的高速缓存读取/写入数据;当运算结束之后,再将高速缓存中的数据刷新到内存当中

随着 CPU 能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。

按照数据读取顺序和与 CPU 结合的紧密程度,CPU 缓存可以分为:一级缓存(L1),二级缓存(L2),部分高端 CPU 还具有三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分【三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的】

有了多级缓存之后,程序的执行就变成了:

当 CPU 要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找

  • 单核 CPU:只含有一套L1,L2,L3缓存
  • 多核CPU:每个核都含有一套L1(甚至 L2)缓存,而共享L3(或者 L2)缓存

下图为一个单 CPU 双核的缓存结构:

在这里插入图片描述

1.2、缓存一致性问题

随着计算机能力不断提升,开始支持多线程。那么问题就来了。

分别来分析下单线程、多线程在单核 CPU、多核CPU中的影响:

  • 单线程:CPU 核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题
  • 单核CPU,多线程:进程中的多个线程会同时访问进程中的共享数据,CPU 将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突
  • 多核CPU,多线程:每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的 cache 中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的 cache 之间的数据就有可能不同

在 CPU 和主存之间增加缓存,解决了 CPU 和主存速率不匹配的问题。但在多线程场景下就可能存在缓存一致性问题:在多核 CPU 中,每个核的的缓存中,关于同一个数据的缓存内容可能不一致

1.3、处理器优化和指令重排

除了上述情况,还有一种硬件问题也比较重要:为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化

除了处理器会对代码进行优化处理,很多现代编程语言的编译器也会做类似的优化【为了提高性能】,比如像 Java 的即时编译器(JIT)会做指令重排序

从源码到最终执行的指令序列的示意图:
在这里插入图片描述

重排序可以分为三种类型:

  • 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
  • 指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
  • 内存系统的重排序:由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

可想而知,如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题

2、并发编程的三大问题

在并发编程中有这三大问题:

  • 原子性问题:处理器优化
  • 可见性问题:缓存一致性问题
  • 有序性问题:指令重排

2.1、原子性问题

原子性问题:多线程场景中操作如果不能保证原子性,会导致处理结果和预期不一致

线程是 CPU 调度的基本单位。CPU 有时间片的概念,会根据不同的调度算法进行线程调度。所以在多线程场景下,就会发生原子性问题。

因为线程在执行一个读改写操作时,在执行完读改之后,时间片耗完,就会被要求放弃 CPU,并等待重新调度。这种情况下,读改写就不是一个原子操作

在单线程中,一个读改写就算不是原子操作也没关系,因为只要这个线程再次被调度,这个操作总是可以执行完的。但是在多线程场景中可能就有问题了。因为多个线程可能会对同一个共享资源进行操作

如:i++ 操作 。一共有三个步骤:loadaddsave。共享变量就会被多个线程同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致

2.2、有序性问题

除了引入了时间片以外,由于处理器优化和指令重排等,CPU 还可能对输入代码进行乱序执行,比如:load => add => save 有可能被优化成 load => save => add 。这就是有序性问题

2.3、可见性问题

可见性问题就是上述的缓存一致性问题

2.4、三大特性

所以,在并发编程时,为了保证数据的安全,需要满足以下三个特性:

  • 原子性:在一个操作中,CPU 不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行
  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
  • 有序性:序执行的顺序按照代码的先后顺序执行

3、什么是内存模型?

3.1、概念

上述问题是因硬件的不断升级导致的。那么,有没有什么机制可以很好的解决上面的这些问题呢? —— 内存模型

为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。 通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性【与处理器/缓存/并发/编译器有关】。它解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

内存模型解决并发问题主要采用两种方式:限制处理器优化使用内存屏障

3.2、内存模型到底是怎么保证缓存一致性的呢?

为了解决前面提到的缓存数据不一致的问题,人们提出过很多方案,通常来说有以下 2 种方案:

  • 通过在总线加 LOCK# 锁的方式:因为 CPU 和其他部件进行通信都是通过总线来进行的,如果对总线加 LOCK# 锁的话,也就是说阻塞了其他 CPU 对其他部件访问(如内存),从而使得只能有一个 CPU 能使用这个变量的内存。在总线上发出了 LCOK# 锁的信号,那么只有等待这段代码完全执行完毕之后,其他 CPU 才能从其内存读取变量,然后进行相应的操作
    • 问题:在锁住总线期间,其他CPU无法访问内存,会导致效率低下
  • 通过缓存一致性协议(Cache Coherence Protocol)【MESI 协议】:当 CPU 写数据时,如果发现操作的变量是共享变量,即在其他 CPU 中也存在该变量的副本,会发出信号通知其他 CPU 将该变量的缓存行置为无效状态,因此当其他 CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取,保证了每个缓存中使用的共享变量的副本是一致的

3.3、缓存一致性协议 —— MESI 协议

在 MESI 协议中,每个缓存可能有有 4 个状态,它们分别是:

  • M(Modified):这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本 Cache 中
  • E(Exclusive):这行数据有效,数据和内存中的数据一致,数据只存在于本 Cache 中
  • S(Shared):这行数据有效,数据和内存中的数据一致,数据存在于很多 Cache 中
  • I(Invalid):这行数据无效

传统的 MESI 协议中有两个行为的执行成本比较大:

  1. 将某个 Cache Line 标记为 Invalid 状态
  2. 当某 Cache Line 当前状态为 Invalid 时写入新的数据

所以,CPU 通过 Store Buffer 和 Invalidate Queue 组件来降低这类操作的延时。如图:

在这里插入图片描述

当一个 CPU 进行写入时,首先会给其它 CPU 发送 Invalid 消息,然后把当前写入的数据写入到 Store Buffer 中。然后异步在某个时刻真正的写入到 Cache 中。当前 CPU 核如果要读 Cache 中的数据,需要先扫描 Store Buffer 之后再读取 Cache。但是,此时其它 CPU 核是看不到当前核的 Store Buffer 中的数据的,要等到 Store Buffer 中的数据被刷到了 Cache 之后才会触发失效操作

而当一个 CPU 核收到 Invalid 消息时,会把消息写入自身的 Invalidate Queue 中,随后异步将其设为 Invalid 状态,和 Store Buffer 不同的是,当前 CPU 核使用 Cache 时并不扫描 Invalidate Queue 部分,所以可能会有极短时间的脏读问题。

MESI 协议,可以保证缓存的一致性,但是无法保证实时性

4、什么是 Java 内存模型?

4.1、概念

同一套内存模型规范,不同语言在实现上可能会有些差别。

Java 内存模型就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了 Java 程序在各种平台下对内存的访问都能保证效果一致的机制及规范

Java内存模型,一般指的是 JDK 5 开始使用的新的内存模型,主要由 JSR-133: JavaTM Memory Model and Thread Specification 描述

JSR133中文版1.pdf

Java内存模型规定:所有的变量都存储在主内存中,每条线程还有自己的工作内存/本地内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行

如下图:

在这里插入图片描述

JMM 就作用于工作内存和主存之间数据同步过程。它规定了如何做数据同步以及什么时候做数据同步

如:两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 JMM 规范下会有一系列的操作:

在这里插入图片描述
为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:

  1. lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。
  2. unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  3. read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  4. load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  5. use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
  6. assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
  7. store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作
  8. write:写入。作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中

如下图:

在这里插入图片描述

总结:JMM 是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性

4.2、实现

在 Java 中,提供了一系列和并发处理相关的关键字,比如 volatilesynchronizedfinalconcurren 包 等。其实这些就是 Java 内存模型封装了底层的实现后提供给程序员使用的一些关键字。

所以,Java 内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用

4.2.1、原子性

synchronized 关键字保证原子性【底层通过 monitorentermonitorexit 字节码指令实现】。

在 Java 中,可以使用 synchronized 来保证方法和代码块内的操作是原子性的

4.2.2、可见性

volatile 关键字提供的功能:被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。 可以使用 volatile 来保证多线程操作时变量的可见性

  • synchronized:获取锁时,会清除在工作内存中的所有共享变量的副本,并重新从主内存中读取。当一个线程修改了共享变量后,它必须释放锁,并把修改后的值刷新到主内存中,以便其他线程可以看到最新的值
  • final:在初始化后就不会更改,所以只要在初始化过程中没有把 this 指针传递出去也能保证对其他线程的可见性

4.2.3、有序性

  • volatile:禁止指令重排
  • synchronized:保证同一时刻只允许一条线程操作

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

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

相关文章

【Qt】:对话框(一)

对话框 一.基本的对话框二.自定义对话框三.通过图形化界面自定义对话框四.关于对话框mode 对话框是GUI程序中不可或缺的组成部分。一些不适合在主窗口实现的功能组件可以设置在对话框中。对话框通常是一个顶层窗口,出现在程序最上层,用于实现短期任务或者…

小程序项目思路分享爬虫

小程序项目思路分享爬虫 具体需求: 有这几个就行,门店名称门店地址门店类型,再加上省、市、县/区门店名称:storeName 门店地址:storeAddress 程序运行: honor_spider获取经纬度信息。 经纬度——>详…

CentOS上使用cgroup限制进程使用内存

安装cgroup 要使用cgroup首先需要系统支持,需要安装两个rpm包 yum install libcgroup libcgroup-tools 创建限制内存的cgroup组 cgroup组需要在/sys/fs/cgroup/memory目录下创建,我们创建一个限制进程内存大小为10M的cgroup组,这个组中内存…

云计算重要概念之:虚拟机、网卡、交换机、路由器、防火墙

一、虚拟机 (Virtual Machine, VM) 1.主流的虚拟化软件: 虚拟化软件通过在单个物理硬件上创建和管理多个虚拟环境(虚拟机),实现资源的高效利用、灵活部署、隔离安全以及便捷管理,是构建云计算和现代化数据中心的核心…

【Linux】初识Linux,虚拟机安装Linux系统,配置网卡

前言 VMware软件:首先,确保您已经下载了VMware Workstation软件并安装在电脑上。VMware Workstation是一款功能强大的虚拟化软件,它允许在单一物理机上运行多个操作系统。 Linux镜像文件:需要准备一个Linux操作系统的镜像文件。…

华为ensp中PPP(点对点协议)中的PAP认证 原理和配置命令

作者主页:点击! ENSP专栏:点击! 创作时间:2024年4月8日14点31分 PPP协议(Point-to-Point Protocol)是点到点协议,是一种常用的串行链路层协议,用于在两个节点之间建立点…

如何保证消息不丢失?——使用rabbitmq的死信队列!

如何保证消息不丢失?——使用rabbitmq的死信队列! 1、什么是死信 在 RabbitMQ 中充当主角的就是消息,在不同场景下,消息会有不同地表现。 死信就是消息在特定场景下的一种表现形式,这些场景包括: 消息被拒绝访问&am…

全国水科技大会 免费征集《水环境治理减污降碳协同增效示范案例》

申报时间截止到2024年4月15日,请各单位抓紧申报,申报条件及申报表请联系:13718793867 围绕水环境治理减污降碳协同增效领域,以资源化、生态化和可持续化为导向,面向生态、流城、城市、农村、工业园区、电力、石化、钢…

前端mock数据——使用mockjs进行mock数据

前端mock数据——使用mockjs进行mock数据 一、安装二、mockjs的具体使用 一、安装 首选需要有nodejs环境安装mockjs:npm install mockjs 若出现像上图这样的错,则只需npm install mockjs --legacy-peer-deps即可 src下新建mock文件夹: mo…

基于Java SpringBoot+Vue的体育用品库存管理系统

博主介绍:✌IT徐师兄、7年大厂程序员经历。全网粉丝15W、csdn博客专家、掘金/华为云//InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取源码联系🍅 👇🏻 精彩专栏推荐订阅👇&#x1f3…

LeetCode | 数组 | 二分查找 | 35.搜索插入位置【C++】

题目链接 题目描述 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 示例 1: 输入: nums [1,3,5,6], target 5 输出…

C++ 线程库(thread)与锁(mutex)

一.线程库(thread) 1.1 线程类的简单介绍 thread类文档介绍 在C11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C11中最重要的特性就是对线程进行支持了&#xff…

eNSP-抓包解析TCP三次握手和四次挥手的过程

一、环境搭建 1.设备连接 并 启动所有设备 2.服务器配置 3.客服端配置 二、抓包测试 1.打开抓包软件 2.客户端获取数据 三、抓包结果

HEC-HMS水文模型

HEC-HMS是美国陆军工程兵团水文工程中心开发的一款水文模型。HMS能够模拟各种类型的降雨事件对流域水文,河道水动力以及水利设施的影响,在世界范围内得到了广泛的应用。它有着完善的前后处理软件,能有效减轻建模的负担;能够与HEC开…

如何用Vue实现实时网络状态监控:一篇让你轻松掌握前端网络连通性管理的指南

1、演示 2、网络监控目的 网络性能优化: 通过监控用户的网络状态,可以了解网络延迟、带宽利用率、丢包率等信息,从而优化网络性能,提升用户体验。 故障排除: 可以监控网络状态以及网络设备的运行情况,及时…

【现代C++】线程支持库

现代C&#xff08;C11及其之后的版本&#xff09;引入了标准的线程支持库&#xff0c;使得多线程编程变得更加简单和可移植。这个库提供了线程管理、互斥量、条件变量和其他同步原语。 1. std::thread - 基本线程 std::thread允许创建执行特定任务的线程。 #include <ios…

蓝桥杯-油漆面积

代码及其解析:(AC80%&#xff09; 思路:是把平面划成单位边长为1&#xff08;面积也是1&#xff09;的方格。每读入一个矩形&#xff0c;就把它覆盖的方格标注为已覆盖&#xff1b;对所有矩形都这样处理&#xff0c;最后统计被覆盖的方格数量即可。编码极其简单&#xff0c;但…

gradio简单搭建——关键词匹配筛选【进一步优化】

gradio简单搭建——关键词匹配筛选[进一步优化] 任务回顾新的想法&#xff1a;无效元素筛选界面搭建数据处理与生成过程交互界面展示 任务回顾 在 apply \text{apply} apply方法的使用一节中&#xff0c;简单提到了任务目标&#xff1a;通过关键词的形式&#xff0c;在文本数据…

三维点云:对原始点云数据进行体素化

文章目录 一、原始点云二、对原始点云进行体素化三、结果展示 一、原始点云 &#x1f349;原始点云为.pts文件&#xff0c;内容为x, y, z的坐标 原始点云展示 二、对原始点云进行体素化 使用open3d库实现&#xff0c;如果没有需要在命令行执行pip install open3d import o…

【STL】vector

目录 1. vector的使用 1.1 vector的定义 1.2 vector iterator 的使用 1.3 vector 空间增长问题 1.4 vector 增删查改 1.5 vector 迭代器失效问题&#xff08;重点&#xff09; 2.vector模拟实现 1. vector的使用 1.1 vector的定义 1.2 vector iterator 的使用 1.3 vecto…