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

news2024/12/23 19:04:49

目录

  • 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.coloradmin.cn/o/1579618.html

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

相关文章

自动驾驶基础技术-无迹卡尔曼滤波UKF

自动驾驶基础技术-无迹卡尔曼滤波UKF Unscented Kalman Filter是解决非线性卡尔曼滤波的另一种思路,它利用Unscented Transform来解决概率分布非线性变换的问题。UnScented Kalman Filter不需要像Extended Kalman Filter一样计算Jacobin矩阵,在计算量大…

LeetCode-热题100:K 个一组翻转链表

题目描述 给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。 k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。 你不能只是单纯的改变节…

在一台恢复测试机器上验证oracle备份有效性

一 目的 定期将生产环境oracle数据库恢复到一台测试环境数据库服务器上,以验证备份是否有效,是否能正常恢复。 二 环境 这里以恢复orcl1库为例,计划在orcl这个实例上进行恢复测试。 三 实验步骤 3.1 在目标端创建和源端一样的备份目录 ①…

从零开始写 Docker(十)---实现 mydocker logs 查看容器日志

本文为从零开始写 Docker 系列第十篇,实现类似 docker logs 的功能,使得我们能够查查看容器日志。 完整代码见:https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实现有一个大致认识: 核心原理&#x…

51单片机之自己配串口寄存器实现波特率9600

本配置是根据手册进行开发配置的 1、首先配置SCON 所以综上所诉 SCON 0x40 (0100 0000) 2、PCON不用配置 3、配置定时器1 4、波特率的计算 5、配置AUXR 6、对比 7、实现 8、优化(实现字符串) 引入TI (智能延时&…

加州大学欧文分校英语基础语法专项课程03:Simple Past Tense 学习笔记(完结)

Learn English: Beginning Grammar Specialization Specialization Certificate course 3: Simple Past Tense Course Certificate 本文是学习 https://www.coursera.org/learn/simple-past-tense 这门课的学习笔记,如有侵权,请联系删除。…

Java基础笔记(一)

一、面向对象高级基础 1.Java的动态绑定机制 public class DynamicBinding {public static void main(String[] args) {//a 的编译类型 A, 运行类型 BA a new B();//向上转型System.out.println(a.sum());//40 子类sum()注释后-> 30System.out.println(a.sum1());//30 子类…

STM32无刷电机全套开发资料(源码、原理图、PCB工程及说明文档)

目录 1、原理图、PCB、BOOM表 2、设计描述 2.1 前言 2.2 设计电路规范 3、代码 4、资料清单 资料下载地址:STM32无刷电机全套开发资料(源码、原理图、PCB工程及说明文档) 1、原理图、PCB、BOOM表 2、设计描述 2.1 前言 经过一个星期的画PCB,今…

上传应用程序到苹果应用商店的工具和要点

引言 在今天的移动应用市场中,将应用程序上传到苹果应用商店(App Store)是许多开发者的首要任务之一。然而,不同操作系统下的开发者可能需要使用不同的工具和遵循不同的要求来完成这一任务。本文将介绍在 macOS、Windows 和 Linu…

day75 js 正则表达式 window对象轮播图片调用定时器

一 正则表达式: RegExp 对象: 对字符串执行模式匹配的强大工具。 1 创建正则表达式对象 let reg /模式/修饰符 修饰符 attributes 是一个可选的字符串,包含属性 "g"、"i" 和 "m", …

kmeans聚类sklearn实现(Python实验)

Kmeans毫无疑问,好用又“便宜”的算法,经常在很多轻量化场景中实现。所谓的“聚类”(Clustering),就是通过欧氏距离找哪些点构成一个簇。假设我们空间中有一堆点,通过肉眼大概可以看出有两簇,思…

rman 物理误删除的整库恢复

故障模拟: 有备份情况下,物理删除了 rm -rf /oraback/* 下的所有文件1 环境准备 备份 rman target / log/oraback/rmanbak/0_rmanbak.log <<EOF run{ delete noprompt backup; allocate channel c1 type disk maxpiecesize 1000M; allocate channel c2 type disk max…

java实战基础------一文搞定maven

&#x1f388;个人主页&#xff1a;靓仔很忙i &#x1f4bb;B 站主页&#xff1a;&#x1f449;B站&#x1f448; &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;JAVA实战基础 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处…

什么是SD NAND?

文章目录 前言一、SD NAND 简介二、CS创世 SD NAND 的六点优势三、CS创世 SD NAND 相较于其它存储产品的优缺点四、结语 前言 本文转自 雷龙官网 一、SD NAND 简介 什么是CS创世 SD NAND呢&#xff1f;很多的朋友一直想知道这个问题。今天我们雷龙也精心准备了SD NAND 的一个介…

程序员搞副业对自身有提升吗?

一、程序员如何搞副业 程序员利用自身技能开展副业的方式有很多&#xff0c;以下是一些常见的选择&#xff1a; 1. 开发独立软件或APP&#xff1a;如果你有独特的创意或者发现市场上存在某种需求&#xff0c;可以开发独立的软件或手机应用&#xff0c;并通过广告、付费下载或内…

云测了解记录

1.app、web、接口&#xff0c;一般通过特定的录制工具&#xff0c;录制后可在云测平台执行。平台可查看自动执行的过程&#xff0c;生成测试报告等 2.app真机实测 选择机型&#xff0c;类似手机模拟器&#xff0c;可安装app&#xff0c;以及模拟各种操作。实现特定机型bug复现…

HCIP-Datacom(H12-821)题库补充(4月9日)

最新 HCIP-Datacom&#xff08;H12-821&#xff09;完整题库请扫描上方二维码访问&#xff0c;持续更新中。 IS—IS过载标志位是指LSP报文中的OL字段。对设备设置过载标志位后&#xff0c;其它设备在进行SPF计算时不会使用这台设备做转发&#xff0c;只计算该设备的直连路由。 …

计算机服务器中了rmallox勒索病毒怎么办?Rmallox勒索病毒解密流程步骤

网络为企业的生产运营提供便利的同时&#xff0c;也为企业的数据安全带来严重威胁。随着互联网技术的不断应用与发展&#xff0c;企业的生产运营离不开网络&#xff0c;利用网络可以开展各项工作业务&#xff0c;极大地方便了企业生产运营&#xff0c;大大提升了企业生产效率&a…

item_search_shop获得店铺的所有商品API接口显示指定商铺下所有商品列表数据

要获取店铺的所有商品API接口显示指定商铺下所有商品列表数据&#xff0c;首先需要了解API的具体请求方式、参数以及返回数据格式。以下是一个示例&#xff1a; 公共参数 API请求地址: API接口调用key、secret 名称类型必须描述keyString是调用key&#xff08;必须以GET方式…

Leetcode C语言习题

Leetcode习题27&#xff1a;移除元素 题目&#xff1a; 说明&#xff1a; 示例&#xff1a; 题解&#xff1a; 方法一&#xff1a;&#xff08;开辟额外的数组空间&#xff09; 我们可以创建一个新的数组&#xff0c;然后用循环来遍历原数组&#xff0c;将原数组中不为 val…