Java并发编程(02)--CPU和缓存一致性

在继续学习Java并发编程之前, CPU的执行过程以及CPU缓存一致性问题是必须要了解的, 这一部分的内容是Java并发设计的基石, 对后续内容的了解也有非常大的帮助。

1. CPU缓存结构

在比较古老的CPU中, CPU直接和主存进行数据交互。 CPU将主存中的数据取到自己的寄存器中, 计算完毕后再写回主存。 寄存器可以认为是数据的临时存放点, 将所要处理的数据放到自己的手边上, 这样处理起来会更加的快速。

但是随着CPU技术的快速发展, 内存数据的读取和写入速度远远跟不上CPU的处理速度, 这样一来就导致CPU操作内存需要花费很长的时间。 这个过程在生活中其实很常见, 比如两个水泥工, 一个和水泥, 一个用和好的水泥砌墙。 砌墙的这个人操作速度很快, 每次都得等和水泥的把水泥和好才能继续干活儿, 这样一来就会有瓶颈问题。

所以为了解决CPU和内存间的数据处理速度差的这个瓶颈, CPU引入了多级缓存, 目前在市面儿上刨去买不起的intel Core i9, 绝大多数intel第八代CPU均有3级缓存, i7-8700k的第三级缓存更是达到了12M。

速度读写速度:L1 > L2 > L3; 缓存容量:L1 < L2 < L3; 造价成本: L1 > L2 > L3。 可以简单的认为上一级缓存是下一级缓存的一个子集。 图中的实例可能与实际有些偏差, 例如L2 cache可能在两个核之间共享, 也可能是一个核独享, 但是L1 cache必定是核独享, L3 cache为所有核共享。

在有了多级缓存之后, CPU与主存之间的数据交互就变成了: 当程序运行过程中, 会将运算所需要的数据从主存中复制一份放入到CPU的高速缓存中, 至于是放置到哪一个高速缓存, 视数据处理的紧急程度而定。 当CPU要进行运算时, 首先从L1 cache取数据, 没有的话从L2 cache取数据, 如果还是没有的话就从L3 share cache或者主存中取数据。

这样一来, 高速缓存充当一个缓冲区的角色, 协调CPU与内存之间的数据处理, 从而加快整体的数据处理与计算能力。

2. CPU多级高速缓存带来的问题

虽然CPU多级高速缓存能够解决CPU和内存之间的处理速度差问题, 但是又带来了新的问题。 现在CPU架构均设计称为多核多线程模型, 那么当一个进程中存在多个线程运行, 并运行在不同的核心上时, 就会出现这样的问题:每个核心都会在各自的caehe中保留一份共享内存的缓冲。 由于多核是可以并行的, 可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

所以, 在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题, 即每个核的高速缓存中, 对于主存中的同一个数据的缓存内容可能会不一致。

3. 缓存一致性协议

为了解决缓存一致性问题, CPU服务商提供较多的解决方案, 使用最广泛的为Intel MESI协议。

MESI协议核心思想为: 当CPU写数据时, 如果发现该数据为共享变量(其它核心也有该数据的副本), 那么将会发出信号使其它核心关于该数据的缓存失效。 当其它核心想要读取该数据时, 由于缓存已失效, 将会从主存中重新读取。

非常像Cache Aside缓存失效模型: 当数据库发生数据更新时, 将该数据所在的缓存(redis, memcache)删除, 那么下一次读数据时发现没有缓存, 重新从数据库中取出数据并重新设置缓存。

MESI其实是四种状态的首字母组合:

M:Modified, 数据已经被修改, 且缓存与主存数据不一致, 将来会写回主存中
E:Exclusive, 缓存独占该数据, 且缓存数据与主存数据一致。 并且可能会被设置为共享状态(Shared)以及被修改(Modified)
S:Shared, 数据在多个缓存中共享, 且与主存数据一致。 并且可能会被置位失效(Invalid)状态
I:Invalid, 该缓存数据已失效, 不会被使用

MESI之间状态会不断的发生迁移, 包括Local Read(核心读取自己的缓存数据), Local Write, Remote Read以及Remote Write, 设计较为精妙。 事实上, 现在的MESI协议已经更新为MESIF协议, 增加了Forward状态。

MESI之间的状态具体怎么转换, 这些细节不再描述, 这部分内容已经过于底层了, 只需要了解MESI能够解决缓存一致性问题, 但是无法保证数据的实时性即可。

4. Java内存模型

MESI协议解决了多线程下的缓存一致性, 但是并没有为我们解决多线程下变量操作的原子性, 可见性以及有序性, 这3个在并发编程中至关重要的特性是由Java内存模型来实现的。

本文并没有对Java内存模型有深入理解的想法, 相反, 这一部分的内容我认为可以流于表面, 因为内存模型与CPU相关, 与缓存相关, 与编译器相关, 与并发也相关, 整体实现复杂且精妙。 作为一个开发Low Bee, 实在有心无力。

我们只需要知道: Java内存模型是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题

 | center

大致模型如上, 每一个线程都会有其本地内存, 或者称为工作内存。 当线程对变量进行修改时,首先写入本地内存, 而后将其写回主存时才对线程2可见。

volatile关键字所修饰的变量之所以拥有并发可见性, 就是因为被其修饰的变量在修改之后立即同步到主存当中, 在每次使用之前也只从主存中读取。

5. 小结

本篇文章很多内容都只是流于表面, 对更加深层次的原理并没有进行梳理。 这一部分的细节内容实在太多, 我认为掌握了80%的表面, 并且在使用synchronized以及volatile这些关键字时能够理解其基本原理即可。 毕竟我们写的还是业务代码, 并不会与硬件直接接触。