系列介绍
本系列主要重点介绍 Java 中的 J.U.C 并发编程,从原理,理论到实践的过程,带你一步步了解各种知识点,把所有技术点构成一个闭环,形成一个知识体系。
希望在 J.U.C 系列对你有新的了解和认知。
第一步,我想从计算机的底层模型来做为我这个系列的开头,因为你只有理解了计算机的原理和结构,才能对于 Java 的一些设计 (J.U.C,Sync,JMM) 才有更加深刻的理解和使用。
本章节不涉及到 Java 相关的知识点。
现代计算机理论模型
现代计算机模型是基于 - 冯诺依曼计算机模型
也称冯·诺伊曼模型(Von Neumann model)或普林斯顿结构(Princeton architecture),是一种将程序指令存储器和数据存储器合并在一起的计算机设计概念结构。依据冯·诺伊曼结构设计出的计算机称做冯. 诺依曼计算机,又称存储程序计算机。
计算机在运行指令时,会从存储器中一条条指令取出,通过译码(控制器),从存储器中取出数据,然后进行指定的运算和逻辑等操作,然后再按地址把运算结果返回内存中去。
接下来,再取出下一条指令,在控制器模块中按照规定操作。依此进行下去。直至遇到停止指令。
程序与数据一样存贮,按程序编排的顺序,一步一步地取出指令,自动地完成指令规定的操作是计算机最基本的工作模型。这一原理最初是由美籍匈牙利数学家冯. 诺依曼于 1945 年提出来的,故称为冯. 诺依曼计算机模型。
计算机五大核心组成部分
-
控制器(Control):
- 是整个计算机的中枢神经,其功能是对程序规定的控制信息进行解
释,根据其要求进行控制,调度程序、数据、地址,协调计算机各部分工作及内存与外设的访问等。
-
运算器(Datapath)
- 运算器的功能是对数据进行各种算术运算和逻辑运算,即对数据进
行加工处理。
-
存储器(Memory)
- 存储器的功能是存储程序、数据和各种信号、命令等信息,并在需
要时提供这些信息。
-
输入(Input system)
- 略,输入设备有键盘、鼠标器等。
-
输出(Output system)
- 略。打印机等。
上图为计算机模型流程图
-
计算器
- 实际上就是 CPU 的工作
-
存储器
- 计算器中的内存(RAM)
上图的重点只需要看中间的部分,本质的逻辑就是 CPU、存储。CPU 是如何存储数据,计算;CPU、存储是如何交互通信的。
现代计算机硬件结构原理
下图为计算器硬件的结构原理图
拓展槽:指的内存条。
我们可以把重点放到 CPU,I/ O 总线,拓展槽上面,那为何结构是这样设计的?
无论是 CPU、存储器、或者我们的计算器中的显示器、鼠标、键盘都是通过 I / O 总线来做交互通信。
I/ O 总线可以理解为一条高速通道,在这其中,CPU 的频率最高的达到 GHz,而内存条频率远远无法和 CPU 相比拟,而玩过游戏的小伙伴也知道,对计算器的显存也是会在 I / O 总线上面做通信,如此之多的模块都在这上面,而 CPU 又是极高的频率。
所以 CPU 的结构原理就会有一个 CPU Cache
的设计,就是会把收到的指令复制一遍存到 CPU Cache 中,进行计算。
运行速度来对比的:寄存器 > L1 > L2 > L3 > 内存条
,而内存条的读写速率远远小于 CPU Cache,所以这个也是会有 CPU Cache 的设计产生的原因之一。
因为内存条的频率远远小于 CPU,所以才会有了 CPU Cache
的出现,内存条把编译后的指令,通过 I / O 总线放到 CPU Cache
中,进行计算和存储。
CPU
CPU 内部结构划分,主要有三种类型的单元
-
控制单元
- 控制单元是整个 CPU 的指挥控制中心,由指令寄存器 IR(Instruction Register)、指令译码器 ID(Instruction Decoder)和 操作控制器 OC(Operation Controller)等组成,对协调整个电脑有序工作极为重要。它根据用户预先编好的程序,依次从存储器中取出各条指
令,放在指令寄存器 IR 中,通过指令译码(分析)确定应该进行什么操作,然后通过操作控制器 OC,按确定的时序,向相应的部件发出微操作控制信号。操作控制器 OC 中主要包括:节拍脉冲发生器、控制矩阵、时钟脉冲发生器、复位电路和启停电路等控制逻辑。
-
运算单元
- 运算单元是运算器的核心。可以执行算术运算(包括加减乘数等基本运算及其附加运算)和逻辑运算(包括移位、逻辑测试或两个值比较)。相对控制单元而言,运算器接受控制单元的命令而进行动作,即运算单元所进行的全部操作都是由控制单元发出的控制信号来指挥的,所以它是执行部件。
-
存储单元
- 存储单元包括 CPU 片内缓存 Cache 和寄存器组,是 CPU 中暂时存放数据的地方,里面保存着那些等待处理的数据,或已经处理过的数据,CPU 访问寄存器所用的时间要比访问内存的时间短。寄存器是 CPU 内部的元件,寄存器拥有非常高的读写速度,所以在寄存器之间的数据传送非常快。采用寄存器,可以减少 CPU 访问内存的次数,从而提高了 CPU 的工作速度。寄存器组可分为专用寄存器和通用寄存器。专用寄存器的作用是固定的,分别寄存相应的数据;而通用寄存器用。
CPU 寄存器
每个 CPU 都包含一系列的寄存器,它们是 CPU 内内存的基础。CPU 在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为 CPU 访问寄存器的速度远大于主存。
CPU 缓存
即高速缓冲存储器,是位于 CPU 与主内存间的一种容量较小但速度很高的存储器。由于 CPU 的速度远高于主内存,CPU 直接从内存中存取数据要等待一定时间周期,Cache 中保存着 CPU 刚用过或循环使用的一部分数据,当 CPU 再次使用该部分数据时可从 Cache 中直接调用, 减少 CPU 的等待时间,提高了系统的效率。
内存
一个计算机还包含一个主存。所有的 CPU 都可以访问主存。主存通常比 CPU 中的缓存大得多。、
上面的图,指的是内存是如何和 CPU 进行交互工作的,我们大概知道这个内存结构就可以了,希望帮助大家了解一下整体的结构,了解计算机是这样的工作方式的,达成一个认知即可。
问题举例 1
public static void main(String[] args) {
int i = 0;
i = 1 + 1;
System.out.println(i);
}
假如执行命令的 main 方法的时候,CPU、内存会按照如下的流程进行读取存储
1 . 初始化忽略,从 i = 1 + 1
, 开始,内存会把这条指令发送到 CPU 中
- CPU 寄存器会去读取(load)i 的内存地址,然后交由
ALU
进行计算,计算结果(i=2)
会以此缓存到 L1,L2,L3; - CPU 会在空闲的时候再把结果同步到内存到,不会立马同步到内存中,同步的条件,只有在自身的缓存内存空间不足,才会进行写入同步到内存中,那有没有什么办法可以把结果硬性的同步到内存中?
这里先引申出一个概念:MESI 缓存一致性协议
CPU 多核缓存架构
问题举例 2
有两个线程 T1,T2 分别到 CPU1,CPU2 去执行,执行以下代码方法
private static int i = 0;
public static void main(String[] args) {
i +=1;
System.out.println(i);
}
按照上面的结构,每个 CPU 都是独立,并且每个线程都保持有自己的对于 i 的一个副本,也就是i + 1
,每个 CPU 在回写同步数据结果的时候,并不知道其他的 CPU 也在针对 i 的内存地址的结果进行计算回写,所以有可能有出现计算错误。
当各自的线程在 CPU 执行完指令之后,实际的结果并非是 i + 1(T1) + 1(T2)
的结果,有可能是 i = 2
,这个就会出现我们的数据一致性问题。
缓存一致性问题
在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有 MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly 及 DragonProtocol,等等。
总线加锁 (奔腾处理器) 这个是很早之前的一个 CPU 的实现方法,这个的原理就是每次 CPU 要把数据回写到内存中的时候,都需要去总线中获取一个锁,获取锁之后才可能把数据写入到内存中。而没有获取到锁的 CPU 就需要等待,直到获取锁为止。
MESI 协议
Cache line : Cache 中最小缓存单位
-
M
- 状态:修改
-
描述:该 Cache line 有效,数据被修改了,和内存中的数据不一致,数据只存在于本 Cache 中。
- 监听任务:Cache line 必须时刻监听所有试图读该 Cache line 相对就内存的操作,这种操作必须在缓存将 Cache line 写回主内存并将状态变成 S(共享)状态之前被延迟执行。
-
E
- 状态:共享(Shared)
-
描述:该 Cache line 有效,数据和内存中的数据一致,数据存在于很多的 Cache 中
- 监听任务:Cache line 必须监听其他缓存使该 Cache line 无效或者独享该 Cache line 的请求,并将该 Cache line 更换状态为无效(Invaild)。
-
I
- 状态:无效(Invaild)
-
描述:该 Cache line 无效,无法做任何操作,不能回写数据到主内存中。
- 监听任务:无
CPU Cache line 会时时刻刻的去嗅探 BUS(缓存一致性协议),监听是否有新的状态改变,是否有新的指令(#LOCK 等),以此来改变自身的 Cache Line 的状态,以便后续可以做相应的操作。
问题举例 2 - 解决
- T1 会从主内存 load 到指令到 CPU1 中,然后会把对应的 Cache line 状态变成
S(独占)
状态; - T2 也执行同样操作,因为有 2 个 CPU 获取了同一个主内存的数据,所以 T2 的 Cache line 会变成一个
S(Shared)
状态,T1 中的 Cache line 会从S(独占) --> E(共享)
进行转变。 - T1,T2 会以此的把指令从 L3 到 L2 到 L1 再到寄存器,进行计算,然后回写到 L3 中,因为对数据进行修改,T1 中的 CPU 需要对 Cache line 进行 锁定操作,锁定完成之后把状态更改为
M(修改)
状态,此时(i = 2)
;当然 T2 也可以同时进行修改状态为M(状态)
,具体看谁快,CPU 彼此之间也是有时间延迟存在。 - 当 T1 更改完成状态之后,以此同时会发送一个消息到 BUS(缓存一致性协议)中,通知其他的监听该内存的 Cache line。
- 此时,CPU 会有一个指令周期,去进行裁决 Cache line 的状态。T2 的 Cache line 监听到了数据变化
(i = 2)
,会把自身的状态更新为I(Invaild)
状态,无法再更新数据(T2 i = 2)
到主内存中。 - 假如 T2 还想再更新数据到主内存中,需要重新的从主内存中 load 数据
(i = 2)
到 CPU 中,再重新计算 ALU,然后回写到主内存中(i = 2 + 1)
Cache line 状态失效场景
- 当
i
存储长度大于一个 Cache line 的时候,这个时候需要存储到多个 Cache line,这个时候是无法做到 MESI 缓存一致性协议的,只能用总线锁。 - 当 CPU 不支持 MESI
小结
回顾本章,我们了解到了计算机的模型,CPU,内存的交互通信工作流程,以及如何保证缓存一致性(MESI)等知识点。