计算机内存模型:
计算机执行程序时每条指令都是在CPU中执行,执行指令的同时势必会有读写操作,这样就引申出一个问题。程序运行时数据的存储是存储在计算机的主存(物理内存)的,而内存的读取和写入的速度与CPU执行指令的速度差距是很大的,这样就造成了内存交互程序的执行效率大大降低,因而CPU中就有了高速缓存。
也就是说CPU在执行指令时,将主存中的数据复制到高速缓存中,将结果运算完毕,再将运算结果刷新到主存中。
java内存模型:
在多线程中,分为了工作内存,和主内存。
主内存:java内存模型规定所有的变量都存储在主内存中(虚拟机内存的一部分,可与物理内存类比),主内存被所有的线程共享,对于一个共享变量来说,主内存存储的是它的实例本身。
工作内存:每条线程都有自己的工作内存,工作内存保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主存中的变量*。不同线程之间也无法访问对方工作内存中的变量,线程间变量值的传递都需要通过主内存来完成。
在所有线程工作时操作共享资源的时候都是将共享资源从主内存拿到工作内存去操作。当使用完之后会放回主内存当中。那么在线程对私有的工作空间中的数据进行写操作,别的线程并没有读到最新的值,就会出现问题。
工作内存和主内存之间是如何交互的???
关于主内存与工作内存之间具体交互的协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节Java内存模型中定义了八种操作来完成。
1.lock:作用于主内存,把变量标识为线程独占状态。
2.unlock:作用于主内存,解除独占状态。
3.read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
4.load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
5.use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
6.assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
7.store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
8.write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中
volatile关键字作用:
1.保证内存可见性:
如何保证内存一致性:
首先加了关键字的变量保证了以下的规则:
read、load、use动作必须连续出现。加LOCK#锁的方式
assign、store、write动作必须连续出现。
解决缓存一致性方案有两种:
通过在总线加LOCK#锁 总线锁定 -----》只能有一个线程才能获取该资源,其它线程处于阻塞状态这样的效率非常低下。
通过缓存一致性协议
所以我们提出了比总线锁定更优化的方式——缓存一致性协议
第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:
当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
什么是缓存一致性性协议呢?
MESI协议:
是以缓存行(缓存的基本数据单位,在Intel的CPU上一般是64字节)的几个状态来命名的(全名是Modified、Exclusive、 Share or Invalid)。该协议要求在每个缓存行上维护两个状态位,使得每个数据单位可能处于M、E、S和I这四种状态之一,各种状态含义如下:
M:被修改的。
处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。
同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。
E:独占的。
处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改,即与内存中一致。
S:共享的。
处于这一状态的数据在多个CPU中都有缓存,且与内存一致。
I:无效的。
本CPU中的这份缓存已经无效。
一个处于M(被修改的)状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。
一个处于S(共享的)状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。
一个处于E(独占的)状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。
当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取(无效的就要读取新值),并把自己状态变成S(既然是无效那么说明已有cpu对缓存做了修改),如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。
Java提供了volatile关键字保证了内存的可见性,底层通过LOCK#或“缓存锁定”实现。
如果是一个volatile关键字修饰的变量,则会有第二行的汇编代码,这是一条含有lock前缀的代码。带 有lock前缀的代码则会通过LOCK#或通过“缓存锁定”实现线程间的可见性。
2.防止指令重排:
什么是指令重排?
编译器和处理器会通过多种方式比如重排序对代码进行优化,然而在重排序后可能会导致运行结果与预想的不同。 例如单例模式的懒汉式。
如何防止指令重排?
内存屏障:
在每个volatile写操作的后面插入一个StoreStore屏障。 写和写是有序的
在每个volatile写操作的后面插入一个StoreLoad屏障。 写和读
在每个volatile读操作的后面插入一个LoadLoad屏障。 读和读有序的
在每个volatile读操作的后面插入一个LoadStore屏障。 读写有序