并发编程-02并发基础CPU多级缓存和Java内存模型JMM

简介: 并发编程-02并发基础CPU多级缓存和Java内存模型JMM

2019080611330380.jpg


CPU多级缓存

CPU多级缓存概述



20190216111945766.png

为什么CPU缓存会分为一级缓存L1、L2、L3?有什么意义?


CPU的频率非常快,主存Main Memory跟不上。CPU缓存是CPU与内存之间的临时数据交换器,为了解决CPU运行处理速度与内存读写速度不匹配的矛盾——缓存的速度比内存的速度快多了。


上图左侧为简易的高速缓存结构,数据的读取和存储都经过高速缓存Cache,CPU核心与高速缓存有一条特殊的快速通道;主存Main Memory与高速缓存都连在系统总线上(BUS)这条总线还用于其它组件的通信。


在高速缓存出现后不久,系统变得愈加复杂,高速缓存与主存之间的速度差异被拉大,直到加入了L2 Cache ,甚至L3 Cache。它们的作用都是


作为CPU与主内存之间的高速数据缓冲区,L1最靠近CPU核心;L2其次;L3再次。

运行速度方面:L1>L2>L3

容量大小方面:L1<L2<L3


CPU会先在最快的L1中寻找需要的数据,找不到再去找次快的L2,还找不到再去找L3,L3都没有那就只能去内存找了。如上图右侧。

20190216114826427.png



CPU 多级缓存-缓存一致性协议MESI

MESI协议的作用:用于保证多个CPU Cache之间缓存共享数据的一致


MESI 是指4中状态的首字母。每个Cache line有4个状态,可用2个bit表示,它们分别是:

注: 缓存行(Cache line):缓存存储数据的单元。


20190216130812925.png


image.png


触发事件:


image.png


CPU 多级缓存-乱序执行优化-重排序


处理器为提高运算速度而做出违背代码原有顺序的优化, 线程下的情况下可见性就会出现问题。


在正常情况下是不对结果造成影响的。在单核时代处理器对结果的优化保证不会远离预期目标,但是在多核环境下却并非如此. 因为在多核条件下会有多个核执行指令,因此每个核的指令都有可能会乱序。另外处理器还引入了L1、L2缓存机制,这就导致了逻辑上后写入的数据不一定最后写入。


在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分为下面三种:


编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。


指令级并行的重排序。现代处理器采用了指令集并行技术来将多条指令重叠执行。如果不存在数据依赖,处理器可以改变语句对应机器指令的执行顺序。


内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能可能是在乱序执行。


20190216134101348.png


编译器重排序属于编译器重排序,指令级并行重排序和内存系统重排序属于处理器重排序,这些重排序可能会导致多线程出现内存可见性问题。


JAVA内存模型 (JMM)


上面讲的是硬件CPU的多级缓存,为了屏蔽掉各种系统硬件和操作系统的内存访问差异,以实现Java程序在各大平台都能达到一致的并发效果,Java虚拟机因此定义了Java内存模型,它规范了Java虚拟机与计算机是如何协同工作的。


接着说上面的重排序,对于JVM来讲是怎样的呢?


对于编译器,JMM的编译器重排序规则会禁止特点类型的重排序。

对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特点类型的处理器重排序。

JMM属于语言级的内存屏障,它确保在不同的编译器和不同的处理器平台上,通过禁止特点类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。


那JMM(Java Memory Model)



20190216134957288.png


关于JVM内存区域中的堆栈请参考 JVM-01Java内存区域与内存溢出异常(上)【运行时区域数据】

存在堆上的对象,可以被持有这个对象的引用的线程访问。如果两个线程同时访问同一个对象的私有变量,此时这两个线程所拥有的是"这个对象的私有拷贝"。 比如这里的Object3


计算机硬件架构简易图示


20190216141637597.png

CPU:一个计算机一般有多个CPU,一个CPU还会有多核。因此意味着每个CPU可能都会运行一个线程,所以计算机出现多线程是很有可能的。


CPU Registers(寄存器):每个CPU都包含一系列的寄存器,它们是CPU内存的基础,CPU在寄存器上执行的 速度远大于在主存上执行的速度,这是因为计算机访问寄存器的速度远大于主存。


CPU Cache(高速缓存):由于计算机的存储设备与处理器的处理设备有着几个数量级的差距,所以现代计 算机都会加入一层读写速度与处理器处理速度接近相同的高级缓存来作为内存与处理器之间的缓冲,将运算使用到的数据复制到缓存中,让运算能够快速的执行,当运算结束后,再从缓存同步到内存之中,这 样,CPU就不需要等待缓慢的内存读写了主(内)存 。 一个计算机包含一个主存,所有的CPU都可以访问主存,主存比缓存容量大的多(CPU访问缓存层的速度快于访问主存的速度!但通常比访问内存寄存器的速度还是要慢点)。


通常情况下,当一个CPU要读取主存(RAM - Main Mernory)的时候,它首先会将主存中的数据读 取到CPU缓存中,甚至将缓存内容读到内部寄存器里面,然后再寄存器执行操作,当运行结束后,会将寄存器中的值刷新回缓存中,并在某个时间点将值刷新回主存。


JAVA内存模型与硬件架构之间的关系


20190216141658687.png

右侧的硬件内存模型是没有区分线程 Stack栈 和 Heap堆,对于硬件而言,所有的栈和堆分布在主存里面,部分栈和堆也可能出现在CPU缓存以及CPU内部的寄存器中。


Java内存模型的抽象结构


2019021614281641.png


如果线程A和线程B要通信的话,必须要经历下面两个步骤

  1. 线程A把本地内存A中跟新过的共享变量刷新到主内存中去
  2. 线程B到主内存中去读取线程A更新过的共享变量

使用如下示意图更加清晰


20190216145804683.png


假设这3个内存中x均为0,线程A执行时将更新后的值假设更新为1临时存放到自己的本地内存A中。 当线程A和B需要通信时,线程A首先会把自己本地内存中的修改后的值即1刷新到主内存中,此时主内存中x=1. 随后线程B到主内存中去读取线程A更新后的值,此时线程B的本地内存的x值也变成了1. 【正常情况 A先B后】


如果线A和线程B同时读取到主内存中的x值,均为0 ,线程A将x值更新为1,放到线程A本地内存,因为线程A和线程B它们之间的数据不可见,线程B并没有等线程A写回主内存之后做更新操作 ,此时线程B也做了同样的更新操作,这个时候线程B的本地内存中x也变成了1 ,因此当线程B操作完成将结果1写回主内存时计数就出现了错误【因为线程B并没有等线程A将更新后数据写会主内存】,正确的情况应该是线程B读取主内存中的1,然后更新为2,再次写会主内存,主内存最后的x=2. 这就引起了并发问题。这就解释了我们案例中的count为啥不总是等于1万的情况 , 案例-> https://blog.csdn.net/yangshangwei/article/details/87400938#_48 【异常情况 AB同时执行】


所以需要使用同步的手段去确保程序处理的准确性。


从整体上看,这两个步骤实质上是线程A向线程B发送消息,而且这个通信过程必须要经过主内存。 JMM通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证 。


Java内存模型的同步八种操作


20190216152542234.png


Lock(锁定):作用于主内存的变量,把一个变量标识变为一条线程独占状态


Unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定


Read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用


Load(载入):作用于工作内存的变量,它把Read操作从主内存中得到的变量值放入工作内存的变量副本中


Use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎


Assign(赋值):作用于工作内存的变量,它把一个从执行引擎接受到的值赋值给工作内存的变量


Store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作


Write(写入):作用于主内存的变量,它把Store操作从工作内存中一个变量的值传送到主内存的变量中


Java内存模型 - 同步规则


如果要把一个变量从主内存中赋值到工作内存,就需要按顺序得执行read和load操作,如果把变量从工作内 存中同步回主内存中,就要按顺序得执行store和write操作,但java内存模型只要求上述操作必须按顺序执行,没有保证必须是连续执行,也就是说Read和Load、Store和Write之间是可以插入其它指令的

不允许read和load、store和write操作之一单独出现

不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中

不允许一个线程无原因地(也就是说必须有assgin操作)把数据从工作内存同步到主内存中

一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了load和assign操作

一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以同时被一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会解锁,lock和unlock必须成对出现

如果一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎中使用这个变量前需要重新执行 load或assign操作初始化变量的值

如果一个变量事先没有被lock操作锁定,则不允许它执行unlock操作,也不允许去unlock一个被其它线程锁定的变量

对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(其实就是执行store和write操作之后)


并发编程优缺点



20190216163137985.png


代码


https://github.com/yangshangwei/ConcurrencyMaster


相关文章
|
5天前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
JVM简介—1.Java内存区域
|
2月前
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
654 166
|
12天前
|
存储 设计模式 监控
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
|
17天前
|
存储 IDE Java
java设置栈内存大小
在Java应用中合理设置栈内存大小是确保程序稳定性和性能的重要措施。通过JVM参数 `-Xss`,可以灵活调整栈内存大小,以适应不同的应用场景。本文介绍了设置栈内存大小的方法、应用场景和注意事项,希望能帮助开发者更好地管理Java应用的内存资源。
29 4
|
1月前
|
人工智能 运维 监控
2025年阿里云服务器配置选择全攻略:CPU、内存、带宽与系统盘详解
在2025年,阿里云服务器以高性能、灵活扩展和稳定服务助力数字化转型,提供轻量应用服务器、通用型g8i实例等多样化配置,满足个人博客至企业级业务需求。针对不同场景(如计算密集型、内存密集型),推荐相应实例类型与带宽规划,强调成本优化策略,包括包年包月节省成本、ESSD云盘选择及地域部署建议。文中还提及安全设置、监控备份的重要性,并指出未来可关注第九代实例g9i支持的新技术。整体而言,阿里云致力于帮助用户实现性能与成本的最优平衡。 以上简介共计238个字符。
|
22天前
|
Java Shell 数据库
【YashanDB 知识库】kettle 同步大表提示 java 内存溢出
【问题分类】数据导入导出 【关键字】数据同步,kettle,数据迁移,java 内存溢出 【问题描述】kettle 同步大表提示 ERROR:could not create the java virtual machine! 【问题原因分析】java 内存溢出 【解决/规避方法】 ①增加 JVM 的堆内存大小。编辑 Spoon.bat,增加堆大小到 2GB,如: if "%PENTAHO_DI_JAVA_OPTIONS%"=="" set PENTAHO_DI_JAVA_OPTIONS="-Xms512m" "-Xmx512m" "-XX:MaxPermSize=256m" "-
|
3月前
|
Java 对象存储 开发者
如何找出Java进程占用CPU高的元凶
本文记录了一次Java进程CPU占用率过高的问题和排查思路。
|
3月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
3月前
|
存储 缓存 前端开发
JavaEE初阶——初识EE(Java诞生背景,CPU详解)
带你从零入门JAVAEE初阶,Java的发展历程认识什么是cpu,cpu的工作原理,cpu是如何进行计算的,cpu的架构,指令集,cpu的核心,如何提升cpu的算力,cpu的指令,,cup的缓存,cpu的流水线
|
3月前
|
存储 监控 算法
Java内存管理的艺术:深入理解垃圾回收机制####
本文将引领读者探索Java虚拟机(JVM)中垃圾回收的奥秘,解析其背后的算法原理,通过实例揭示调优策略,旨在提升Java开发者对内存管理能力的认知,优化应用程序性能。 ####
70 0