聊聊高并发(三十四)Java内存模型那些事(二)理解CPU高速缓存

在上一篇聊聊高并发(三十三)从一致性(Consistency)的角度理解Java内存模型 我们说了Java内存模型是一个语言级别的内存模型抽象,它屏蔽了底层硬件实现内存一致性需求的差异,提供了对上层的统一的接口来提供保证内存一致性的编程能力。

在一致性这个问题域中,各个层面扮演的角色大致如下:

1. 一致性模型,定义了各种一致性模型的理论基础

2. 硬件层,提供了实现某些一致性模型的硬件能力。硬件在默认情况下按照最基本的方式运行,比如

对同一个线程没有数据依赖的指令可以重排序优化执行,有数据依赖的指令按照程序顺序执行,从而保证单线程程序运行的正确性保证读操作读到的数据肯定是之前在同一位置写入的数据

3. 语言层,少数语言提供了语言层面的满足一致性模型的编程能力,另外一些语言则直接使用硬件层提供了一致性编程的能力。提供一致性能力语言的工作方式如下:

把满足一致性需求的编程能力作为一种资源,指定一些规则,比如volitile, synchronized,Happens-before规则等当应用层需要使用这种编程能力的时候,需要显式地提出申请,比如显式地使用volatile来标识变量通过编译器适配底层各种硬件平台提供了一致性编程的能力,比如有些平台使用内存屏障,有些平台使用read-modified-write,需要语言层来屏蔽这种差异性

4. 应用层,比如分布式系统,比如并发的服务器程序,它们在一致性问题中的工作有

根据实际需求来定义应用所需要满足的一致性需求定义和选择相应的实现一致性需求的算法,比如分布式存储中通过消息协议实现的Paxos,Zab,多阶段提交等利用编程语言提供了基本的一致性编程的能力作为实现一致性需求算法的基础

说了一堆一致性需求相关的,那么问题来了,为什么有内存一致性的这个需求呢?

内存一致性需求的出现主要是因为多核CPU的出现,并且存在多级的高速缓存,这样就出现了对内存读写的并发问题,从而出现了内存的一致性问题。

所以高速缓存是造成内存一致性问题的一个重要原因。很多写Java内存模型的文章笼统的说CPU写操作的时候存在一个写缓冲区write buffer,导致写操作不能及时写回到主存,造成了其他线程不能看到新写入的值,也就是所谓的可见性问题; 并且由于写缓存区是一种lazy write,导致了CPU可以在写没有刷新到内存的时候就开始后续的读,也形成了重排序的场景,所谓的有序性的问题。

这篇文章写写CPU高速缓存相关的工作原理,来看看写缓存区到底是个什么东西。本人不是研究硬件的,一些观点也是基于自己的理解,如果说的不对请进一步查阅资料。

先来看一张图,这张就是Java内存模型的概念模型图,工作内存 work memory是对CPU寄存器和高速缓存的抽象。

再来看一张图,摘自《深入理解计算机系统》中描述Intel Core i7处理器的高速缓存的概念模型。

对比这两张图,我们可以看到Java内存模型中每个线程的工作内存实际上就是寄存器以及高速缓存的抽象。在目前主流的多核处理器设计中,一般每个核心都会包含1个L1缓存和L2缓存,多个核心共享一个L3高速缓存。各个核心直接通过系统总线连接。系统总线包括数据总线,地址总线,控制总线,统称系统总线。我们要记住的是总线是一种共享的资源,如果不合理的使用,比如聊聊高并发(五)理解缓存一致性协议以及对并发编程的影响 这篇中说的缓存一致性协议导致的总线流量风暴,会影响程序执行的效率。

这张图说了各级高速缓存的一些参数,有几个要点:

1. CPU只直接和寄存器已经L1缓存交互

2. 现代的L1缓存分为两个单独的物理块:

i-cache存储指令,是只读的,d-cache存储数据,是读写的

3. L2和L3缓存存储指令和数据

4. 注意高速缓存的大小,Core i7的L1缓存大小为64KB, L2缓存是256KB,L3是8MB

5. 缓存是分块,分组的

6. L1的访问周期是4, L2是L1的3倍,L3是L2的3倍

7. 一次内存访问的时钟周期是L3的3倍左右,和L1差2个数量级

8. 一次硬盘(普通磁盘)访问的时间在1-10ms级别,和一次内存访问差4个数量级,和1次高速缓存访问差6个数量级以上

9. 一次固态硬盘访问的时间在10-100微秒级别,比普通硬盘快1到2个数量级,和一次内存访问差2-3个数量级左右

说到高速缓存就不得不说到计算机领域的局部性原理(Principle of Locality)。局部性原理是缓存技术的底层理论基础。局部性包括两种形式:

1. 时间局部性,一个具有良好时间局部性的程序中,被引用过一次的存储器位置很可能在不远的将来再被多次引用

2. 空间局部性,一个具有良好空间局部性的程序中,如果一个存储器位置被引用了一次,那么程序很可能在不远的将来引用附近的一个存储器位置

我们知道64位机器一次内存数据读取64位,也就是8个字节,8个连续的内存位置,所以高速缓存中存放的也是连续位置的数据,这是局部性的体现

局部性对编程的一些指导:

1. 重复引用同一个变量具有良好的时间局部性

2. 对于具有步长为k的引用模式的程序,步长越短空间局部性越好。尤其是操作数组,多维数组,局部性的影响很大

3. 对于取指令来说,循环有好的时间和空间局部性,循环体越小,循环次数越多,局部性越好

另外来看一下存储器的体系结构

志在山顶的人,不会贪念山腰的风景。

聊聊高并发(三十四)Java内存模型那些事(二)理解CPU高速缓存

相关文章:

你感兴趣的文章:

标签云: