1.什么是缓存一致性问题?如何解决呢?
首先先理解java的内存模型.Java 内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。
也就是说,如果一个变量在多个 CPU 中都存在缓存(一般在多线程编程时才会出现),多个CPU同时处理一个变量那么就可能存在缓存不一致的问题。
为了解决缓存不一致性问题,通常来说有以下 2 种解决方法:
1)通过在总线加 LOCK#锁的方式
2)通过缓存一致性协议
在早期的 CPU 当中,是通过在总线上加 LOCK#锁的形式来解决缓存不一致的问题。因为 CPU 和其他部件进行通信都是通过总线来进行的,如果对总线加 LOCK#锁的话,也就是说阻塞了其他 CPU 对其他部件访问(如内存),从而使得只能有一个 CPU 能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了 LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他 CPU 才能从变量 i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。
但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU 无法访问内存,导致效率低下。所以就出现了缓存一致性协议。该协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当 CPU 向内存写入数据时,如果发现操作的变量是共享变量,即在其他 CPU 中也存在该变量的副本,会发出信号通知其他 CPU 将该变量的缓存行置为无效状态,因此当其他 CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
2.简述 volatile 关键字(或 volatile 的内存语义或 volatile 的 2个特性)。
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:
1)可见性. 保证了不同线程对这个变量进行读取时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(volatile 解决了线程间共享变量的可见性问题)。
第一:使用 volatile 关键字会强制将修改的值立即写入主存;
第二:使用 volatile 关键字的话,当线程 2 进行修改时,会导致线程 1 的工作内存中缓存变量 stop 的缓存行无效(反映到硬件层的话,就是 CPU 的 L1或者 L2 缓存中对应的缓存行无效);
第三:由于线程 1 的工作内存中缓存变量 stop 的缓存行无效,所以线程 1再次读取变量 stop 的值时会去主存读取。那么,在线程 2 修改 stop 值时(当然这里包括 2 个操作,修改线程 2 工
作内存中的值,然后将修改后的值写入内存),会使得线程 1 的工作内存中缓存变量 stop 的缓存行无效,然后线程 1 读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。那么线程 1 读取到的就是最新的正确的值。
2)有序性. 禁止进行指令重排序,阻止编译器对代码的优化。
volatile 关键字禁止指令重排序有两层意思:
I)当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
II)在进行指令优化时,不能把 volatile 变量前面的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。
3 volatile的底层实现机制
如果把加入volatile关键字的变量和未加入volatile关键字的变量都生成汇编代码,会发现加入volatile关键字的变量多了一个lock前缀指令.lock前缀指令相当于一个内存屏障,内存屏障有以下功能:1.禁止指令重排序.在lock指令前面的指令必须先执行,在lock指令之后的指令必须比lock指令修饰的变量后执行.2.可见性.在读指令之前插入读屏障,会将工作内存中的缓存数据失效,使之重新从主存中加载数据.在写指令之后插入写屏障,会将写入缓存的数据刷新到主存中.