1 前言

java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM中,JVM执行字节码,最终需要转化为汇编指令在CPU上执行。

指令的执行过程中势必会涉及到数据的读取和写入,CPU执行速度很快,而程序运行过程中的临时数据是存放在主存(物理内存)当中的,程序从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

2 缓存一致性问题

 int i=0;
 i=i+1;
2.1 一般的操作

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

2.2 多线程中的操作 缓存一致性问题

比如同时有2个线程执行这段代码,i的初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,这时两个线程的高速缓存当中i的值依然为0,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。最后,内存中 i的值为1,这就是缓存一致性问题。

3 解决缓存一致性问题

3.1 通过在总线加LOCK#锁的方式(硬件层面)

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

3.2 缓存一致性协议(硬件层面)

例如 Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的,它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。