一、机器硬件CPU
在计算机中,所有的运算操作都是有CPU的寄存器来完成的,CPU指令的执行过程需要涉及数据的读写操作,CPU所能访问的所有数据只能是计算机的主存,虽然CPU的发展频率不断提升,但是内存在访问速度上并没有多大的突破,因此CPU的处理速度和内存的访问速度之间的差距越来越大。
1、CPU 缓存
由于CPU和内存的速度严重不对等,如果CPU直接访问主内存,那么严重拖了CPU的后腿,于是为了提供CPU的吞吐量,于是就在CPU和主内存之间增加了缓存的设计,现在缓存的数量都增加到了 3 级,最靠近 CPU 的缓存称为 L1,然后依次是 L2,L3 和主内存。CPU的缓存模型如下图所示:
CPU 缓存 有是有很多个 缓存行(Cache Line) 构成的,缓存行可以认为是 CPU 缓存中的最小缓单位,目前主流CPU 缓存的缓存行大小为 64 字节。
CPU 缓存的出现是为了解决 CPU 直接访问内存效率低下的问题,程序运行的不在大致为:
- 把运算所需的数据从主内存中复制一份到 CPU 缓存中。
- CPU 进行计算时直接对 CPU 缓存中的数据进行读取和写入。
- 当运算结束后,先把计算结果写入到 CPU 缓存中。
- 之后会再将CPU 缓存中的最新数据刷新到主内存中。
CPU 通过访问 CPU 缓存的方式代替直接访问主内存的方式极大地提高了 CPU 的吞吐能力,有了 CPU 缓存之后,CPU 和主内存之间的架构如下所示:
2、CPU 缓存一致性问题
由于缓存的出现,极大地提高了CPU的吞吐能力,但是,同时也以后入了缓存不一致的问题。比如 i++ 操作,在程序的运行过程中,首先需要将内存中的数据复制一份存放到 CPU 缓存中,那么 CPU 寄存器在进行计算的时候就直接到 缓存中读取和写入,当整个过程运算结束之后,再将 缓存中的数据刷新到主内存中,具体步骤如下:
- 读取主内存中的 i 值到 CPU 缓存中。
- 对 i 进行加一操作。
- 将结果写回到 CPU 缓存中。
- 将数据刷新到主内存中。
i++ 在单线程的情况下不会出现任何问题,但是多线程情况下就会有问题,因每个线程都有自己的工作内存(本地内存,对应 CPU 中的 缓存),变量 i 会在多个线程的本地内存中都存在一个副本。如果同时有两个线程执行 i++ 操作,假设 i 的初始值为 0,每个线程都从主内存中获取 i 的值存入到 CPU 缓存中,然后经过计算再写入主内存中,很有可能 i 经过两次自增之后结果还是 1,这就是经典的缓存不一致性问题。
为了解决缓存一致性问题,主流的解决方法有如下两种方式:
- 通过总线加锁的方式
- 通过缓存一致性协议
第一种方式是一种悲观的实现方式,CPU 和其他组件的通信都是通过总线进行的,如果采用总线加锁的方式,则会阻塞其他 CPU 对其他组件的访问,从而使得只有一个 CPU(抢到总线锁) 能够访问这个变量的内存。所以就有了第二种通过缓存一致性协议的方式来解决不一致的问题。如下图所示:
在缓存一致性就是保证了每一个缓存中使用的共享变量副本都是一致性的,其意思就是,当 CPU 在从中 缓存中的数据时,如果发现该变量是一个共享变量,也就是说在其他的 CPU缓存 中也存在一个副本,那么就进行如下操作:
- 读取操作,不做任何处理,只是将 缓存中的数据读取到寄存器。
- 写入操作,发出信号通知其他 CPU 将该变量的 缓存行置为无效状态,其他 CPU 在进行该变量读取的时候不得不到主内存中再次获取。
二、Java内存模型
Java的内存模型(Java Memory Mode,JMM)指定了 Java 虚拟机如何与计算机的主内存进行工作,理解 Java 内存模型对于编写正确的并发程序是非常重要的。
Java 的内存模型 决定了一个线程对共享变量的写入何时对其他线程可见,Java 内存模型定义了线程和主内存之间的抽象关系,具体如下:
- 共享变量存储于主内存中,每个线程都可以访问。
- 每个线程都有私有的工作内存或者成为本地内存。
- 工作内存只存储该线程对共享变量的副本。
- 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存。
- 工作内存和 Java 内存模型一样也是一种抽象的概念,他起始并不存在,它涵盖了缓存、寄存器、编译器优化以及硬件等。
Java 的内存模型是一个抽象的概念,其余计算机硬件的结构并不完全一样, 比如计算机物理内存不会存在栈内存和堆内存的划分,无论是堆内存还是虚拟机栈内存都会对应到物理主内存,如下图所示 就是 Java内存模型与 CPU 硬件架构的交互图。
三、总结
本章内存主要讲解了 CPU 和 主内的速度差异而引入了 CPU缓存的,从而导致了多线程之间缓存一致性问题,CPU 是通过让缓存失效的方式,来解决缓存一致性问题。在Java内存模型规定了在多个线程操作共享变量是,必须先把主内存数据加载到线程本地内存中,才进行计算操作,这样也会导致多个线程之间数据不一致的问题。那么为了解决这些问题Java有采用了那些技术呢,请看下回分解。