系列介绍
本系列主要重点介绍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)等知识点。