NO.002-Java 并发编程之多核硬件架构
艺术来源于生活,却又高于生活。—— 车尔尼雪夫斯基
这篇文章是 Java 并发编程思想系列的第二篇,本文以概览的方式,从 CPU 硬件架构和操作系统对多 CPU 的支持两个维度阐述对 Java 并发的影响和关系。操作系统将丑陋的硬件转变成美丽的抽象 [MOS],Java 又将不同的操作系统抽象成统一的内存模型。虽然 Java 从设计之初就引入了内存模型,来屏蔽之下的操作系统和计算机硬件,但是万变不离其宗,只有了解了底层的计算机硬件和操作系统,才能真正理解 Java 并发中核心设计原则和方法。
一、CPU 硬件架构的演进
1.1 CPU 的演进
说到 CPU,还要追溯到 1945 年冯·诺依曼的《 First Draft of Report o the EDVAC》(《第一份草案》),在草案中他提出了现代计算机组成的五大部分 —— 控制器、处理器、存储器、输入和输出设备。其中,CPU 负责指令的读取、解码和执行,存储器负责存储程序和各种数据。自此之后,CPU 就向着性能和功耗方向一路向西狂奔出去(摩尔定律)。
一方面,CPU 从提升时钟频率出发,从 1979 年推出的频率为 5 MHz 的 8808 处理器,到 2011 年 8429 MHz 的 AMD FX“推土机”,CPU 时钟频率的增长速度让人咂舌;另一方面,在 CPU 单核芯片的性能已经很难突破时,人们转换了思路,已经不追求 CPU 的时钟频率更高,而是朝着多核的方向发展。从最初的单处理器结构、多处理器结构、超线程结构、多核结构,一直到现在的多核超线程结构。纵观一部 CPU 演进的历史,就是人类对 CPU 的性能压榨史,也是人类不断追求更高性能的奋斗史。
[图摘自:计算机组成原理]
1.2 高速缓存
CPU 的性能的提升,造成了 CPU 与主存之间性能的差距越来越大。按照“木桶理论”:一只木桶盛水的多少,并不取决于桶壁上最高的那块木块,而恰恰取决于桶壁上最短的那块。系统的设计者被迫设计了高速缓存存储器,来减缓 CPU 与主存之间的性能差距,这就是 L1 高速缓存(一级缓存),又由于程序指令和程序数据的行为和热点分布差异很大,因此 L1 Cache 也被划分成 L1i (i for instruction)和 L1d (d for data)两种专门用途的缓存[程序猿需要知道的那些事]。随着两者的性能差距不断增加,又引入了 L2 高速缓存和 L3 高速缓存。ALU 读取数据时按照就近原则查找 L1,没有再找 L2,这样依次找下去。这几种高速缓存的区别是越远离 CPU 存取速度越慢,存储空间越大。下图是各级高速缓存读取效率的对比:
[图摘自:简要总结计算机各种延时]
对于 CPU 来说,它是不会一个字节一个字节的加载的,因为这非常没有效率,一般来说都是要一块一块的加载的,对于这样的一块一块的数据单位,术语叫 Cache Line。Cache Line 可以简单的理解为 CPU Cache 中的最小缓存单位。目前主流的 CPU Cache 的 Cache Line 大小都是 64Bytes[程序猿需要知道的那些事]。
不过读到这里,小伙伴可能会有疑问,为什么使用高速缓存了就可以有效提升整体性能呢?原因是高速缓存是建立在局部性原理上对处理器做的优化,局部性原理的本质是概率的不均等,如我们熟知的正态分布。这个原理的应用范围很广,除了处理器,操作系统的虚拟地址空间的技术、CDN 数据、微博的热点流量。
局部性原理主要包括:
1. 时间局限性:如果某个信息这次被访问,那它有可能在不久的未来被多次访问
2. 空间局限性:如果某个位置的信息被访问,那和它相邻的信息也很有可能被访问到
1.3 多处理器体系结构
CPU 的寄存器太小了,执行完指令后就忘记了,于是把记忆存储的工作都交给了内存,指令需要通过主存读取。有了多核,就需要解决如何协调多核对内存访问的问题,即解决多核如何分配内存的问题,这就需要多核环境下的内存共享模型来解决。现在最流行的是对称多处理器结构(Symmetric Multi-Processor,简称 SMP)。
[图摘自:《深入理解计算机系统》]
1.4 SMP 缓存一致性
在 SMP 对称多处理器结构下,每个处理器都有自己的缓存。当有来自主存的同一份数据 Cache 在 CPU 中被修改,且同步回主内存时,就需要保证多 CPU 中的缓存与主内存中的数据一致,这就需要多 CPU 之间遵循缓存共享的一致性原则,即在某个 CPU 修改共享数据时,需要通知其他 CPU 数据已经被修改。比较经典的缓存一致性协议就是 intel 公司 X86 体系结构中使用到的 MESI 模型,通过总线嗅探(Bus Snooping)来解决缓存一致性的问题,即把所有的读写请求都通过总线广播给所有的 CPU 核心,各个 CPU 核心去嗅探这些请求,再根据本地的情况进行响应[计算机组成 — MESI 协议]。这里不对 MESI 协议进行分析,后续会专门针对 MESI 协议与 Java 中的 volatile 关键字进行比较。
[图摘自:MESI 协议-极客时间]
二、硬件和操作系统对并发的支持
由于多核的出现,那些原来在单核环境下合适的设计变得不那么正确了。例如在单核中,进程虽然也支持并发操作的能力,不过这种并发是一种伪并发,上期的NO.001-简说Java并发编程史中也提到:“从微观角度看同一时刻只有一个进程在使用 CPU 资源(单核 CPU)”,而在多核环境就不同了,因此先前在单核环境下的原语将不能适应,需要操作系统层面进行修正。
2.1 硬件对并发的支持
所有的软件原语操作都是构建在硬件原子操作的基础上[CMPPOS]。对于原子操作的实现来说,要分开考虑单处理器单核系统,和多处理器系统,多核系统。
对于单处理器单核系统来说,只要保证操作指令序列不被打断即可实现原子操作。对于简单的原子操作,cpu 实现上会提供单条指令,比如 INC 和 XCHG;对于复杂的多条指令可以使用自旋 spinlock 或中断禁用,保证操作指令序列不会在执行的中途受干扰
对于多处理器或者多核的系统,除了需要 spinlock 来保证外,还需要保证不会受到同处理器上其他核。当其他核上执行的指令访问的内存空间,与当前原子操作需要访问的内存空间存在冲突时,就会破坏原子操作的正确性[原子操作是如何实现的]。主要的指令有:x86 架构中通过锁住总线(bus)的指令前缀 LOCK,阻塞其他 cpu 核对相关内存的缓存块的访问的 CAS 指令,还有 MIPS 和 ARM 架构下 LL/SC 指令。
2.2 操作系统对并发的支持
操作系统在硬件并发支持的基础进行了更高级的构建,主要是通过同步互斥机制和进程间通信机制来完成的,主要有以下几种(这些内容我们后续会有专门章节分析):
临界区:通过限制进入对某个临界资源进行操作的程序片段,实现临界资源的排他性
忙等待:进程在得到临界区访问权之前,持续测试而不做其他事情,自旋锁做的就是忙等待
信号量:当一个进程试图获取一个不可用(已被占用)的信号量时,信号量会将其加入等待队列,然后让其睡眠。当信号量被释放时,处于等待队列的中的进程会被唤醒,并获得该信号量。信号量适用于锁会被较长时间占用的情况
管程:由关于共享资源的数据结构及在其上操作的一组过程组成,任一时刻通过只能有一个活跃进程来实现互斥。
三、本质和思想
虽然人类不断的追求更好的性能和功耗,但是计算机不能违背现实世界中的物理定律,也因此才会有了多核的研究方向和高速缓存的被迫设计。Java 运行在操作系统之上,操作系统建立在硬件基础上的,Java 程序最终还是要落在硬件架构上的,这也就是我们为什么要了解计算机原理的目的。
本文作者: 葛一凡
分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
注:所有非本人内容均以[]标注,践行原创,践行知识源头,从我做起。
参考
[荷] Andrew S. Tanenbaum. 现代操作系统[MOS](原书第 4 版). 机械工业出版社
邹恒明.计算机的心智操作系统之哲学原理[CMPPOS]. 机械工业出版社
版权声明: 本文为 InfoQ 作者【葛一凡】的原创文章。
原文链接:【http://xie.infoq.cn/article/3b1a8f416f92047be40ee6616】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论