1、CPU Cache模型
1.1 产生原因
在计算机中,所有的运算操作都是由CPU的寄存器完成的,CPU指令的执行过程需要涉及数据的读取和写入操作,CPU访问的所有数据都来自主存。随着技术进步,CPU的处理速度与内存的访问速度之间的差距越来越大,此时CPU直连内存的访问方式会限制CPU,降低CPU整体的吞吐量,于是就产生了缓存的设计。
1.2 模型结构
CPU Cache的读写速度远高于内存的速度,缓解了CPU直接访问内存效率低下的问题,极大提高了CPU吞吐能力。在程序运行过程中,会将运算所需要的数据从主存复制一份到CPU Cache中,CPU进行计算时直接对Cache中的数据进行读写,当运算结束后,再将Cache中的最新数据刷新到主内存中。
1.3 CPU缓存一致性问题
1 public class IncreaseTest { 2 private static int num = 0; 3 4 public synchronized static void increase() { 5 num++; 6 } 7 8 public static int get() { 9 return num; 10 } 11 12 public static void main(String[] args) throws InterruptedException { 13 new Thread(() -> { 14 for (int i = 0; i < 100000; i++) { 15 increase(); 16 } 17 }).start(); 18 } 19 }
执行以上代码,单线程下结果为10000,但多线程下就会得到小于等于10000的结果。具体原因,在执行num++操作时,会有以下具体过程:
(1) 读取主内存的num到CPU Cache中
(2) 对num进行加一操作
(3) 将结果写回到CPU Cache中
(4) 将数据刷新到主内存中
多线程向下,每个线程都有自己的工作内存(本地内存,对应于CPU中的Cache),则变量num会在多个线程下都存在一个副本。假设现有两个线程在执行num++,都从主存中取得num=0存入CPU Cache中,线程一计算后num=1写入主存,线程二计算后也写入主存,则经过两次自增后结果仍为1,这就是缓存不一致问题。
1.4 解决缓存不一致问题
1.4.1 总线加锁方式
CPU和其他组件通信是通过各种总线进行的,采用总线加锁,每次只有一个CPU访问这个变量的内存,从并行退化为串行执行。即每次只有一个线程获取到num(read主存),并进行自增操作,写回主存后(write主存),完成这一过程后,下一个线程才能获取num执行操作。这种方式效率低下。
1.4.2 缓存一致性协议
其大致思想是,当CPU在操作Cache中的数据时,如果发现该变量是一个共享变量,也就是在其他CPU Cache中也存在一个副本,那么执行以下操作:
(1) 读取操作,不做任何处理,只是将Cache中的数据读取到寄存器
(2) 写入操作,发出信号通知其他CPU将该变量的Cache line置为无效状态,写入成功后,此时其他CPU通过总线嗅探机制得知写入成功,就再次从主存中获取该共享变量。
缓存一致性协议内容很复杂,以上只是大致过程。
2、Java内存模型
Java Memory Mode指定了Jav虚拟机如何与计算机的主存进行工作。Java内存模型决定了一个线程对共享变量的写入何时对其他线程可见,Java内存模型定义了线程和主存之间的抽象关系,具体如下:
(1) 共享变量存储于主存之中,每个线程都可以访问
(2) 每个线程都有私有的工作内存或者称为本地内存
(3) 工作内存只存储该线程对共享变量的副本
(4) 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存
(5) 工作内存和Java内存模型一样也是一个抽象概念,其实并不存在,它涵盖了缓存、寄存器、编译器优化以及硬件等