硬件层面了解可见性:
计算机核心部分是CPU 内存 I/O设备
速度:CPU>内存>I/O设备
为了平衡三者的速度差异,计算机有很多的优化
1:CPU增加了高速缓存
2:操作系统增加了进程,线程,通过CPU的时间片切换最大化的提升CPU使用率
3:编译器的指令优化,更合理的去利用好CPU的高速缓存
CPU高速缓存:
缓存一致性:
有了高速缓存后,每个CPU处理过程是先将计算所需要的数据缓存在CPU高速缓存中,在CPU进行计算时候,直接从高速缓存中读取数据并且在计算完成之后写入到缓存中,在整个运算完成后,再将缓存中的数据同步到主内存,由于在多个CPU中,每个线程可能会运行在不同的CPU内,并且每个线程都拥有自己的高速缓存,同一份数据可能会被缓存到多个CPU中,如果在不同CPU中运行的不同线程看到同一份内存的缓存值不一样就会存在缓存不一致问题
解决缓存不一致问题有两种方式:总线锁和缓存锁
总线锁:在多CPU情况下,当其中一个处理器要对共享内存进行操作时候,在总线上发出一个Lock信号,这个信号使得其他处理器无法通过总线来获取共享内存中数据,这样开销大
缓存锁:
缓存一致性协议:为了达到数据访问的一直,需要各个处理器在访问缓存时候遵循一些协议,常见有MSI,MESI,MOSI等
MESI:
M(Modify)--->表示共享数据只缓存在当前CPU缓存中,并且是被修改状态,也就是缓存的数据和主内存中数据不一致
E(Exclusive)--->缓存独占状态,数据只缓存在当前CPU缓存中,,并且没有被修改
S(Shared)------>数据可能被多个CPU缓存,并且各个缓存中数据和主内存数据一致
I(Invalid)--------->缓存已经失效
在MESI协议中,每个缓存的缓存控制器不仅仅知道自己的读写操作,同时还监听其他Cache的读写操作
CPU读请求:M.E,S状态都可以被读取,I 状态CPU只能从主内存中读取数据
CPU写请求:只有处于M,E状态才可以被写,对于S状态的写需要将其他Cache置为无效后才能写
MESI优化带来的问题:
各个CPU缓存行的状态是通过消息传递来进行的,如果CPU0要对一个缓存中的变量进行写入,首先要发送一个失效的消息到其他缓存了该数据的CPU,并且得到他们的确认回执,CPU0在这段时间内都会处于阻塞状态,为了避免阻塞带来的资源浪费,在CPU中引入了Store Bufferes,这样CPU0只需要在写入共享数据时候,直接将数据写入到store bufferes中,同时发送invalidate小小,然后继续去处理其他指令,当收到其他所有CPU发送了invalidate ack消息时候,再将store bufferes中的数据存储至cache line中,最后再从缓存行同步到主内存
这样优化会存在两个问题:
1:数据何时提交时不确定的,因为他需要等待其他CPU给回复才能进行数据同步,这里是一个异步操作
2:引入store bufferes后,处理会尝试从stroe bufferes中获取值,如果store buffer中有值,则直接从stroe buffer中读取,否则从缓存行中读取
例如:如果value是S状态,而value是E状态,当CPU0执行时候,value=10,它将此值写入store buffer中,同时通知其他CPU将自己里面的value置为无效,CPU0发出通知后继续执行isFinish=true,此时CPU1执行代码,获取到isFinish可能为true,但是value=3,这种情况可以认为是CPU的乱序执行,也可以认为是一种重排序
此时光光在硬件层面是无法处理这种前后的依赖关系,所以在CPU层面提供了memory barrier(内存屏障)的指令,在软件层面在合适地方插入内存屏障
lfence(读屏障):处理器在读屏障之后的读操作都在读屏障之后执行,配合写屏障使得写屏障之前的内存更新对于读屏障之后的操作是可见的
sfence(写屏障):在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存,写屏障之前的指令的结果对屏障之后的读或者写是可见的
mfence(全屏障):确保屏障前的内存读写操作的结果提交到内存后,在执行屏障后的读写操作
volatile关键字会生成一个Lock的汇编指令,这个指令就相当于一中内存屏障
JMM(Java Memory Model)
导致可见性问题的根本原因是缓存以及重排序,而JMM提供了合理的禁用缓存以及禁止重排序的方法,所以核心在于解决有序性和可见性
JMM抽象模型分为主内存,工作内存,主内存是所有线程共享,一般是实例对象,静态字段,数组对象等存储在堆内存中的变量,工作内存是每个线程独占的,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,线程之间的共享变量值的传递都是基于主内存完成
Java内存模型:通过内存屏障禁止重排序,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的CPU指令,对于编译器而言,内存屏障将限制它所能做的重排序优化,对于处理器而言,内存屏障将会导致缓存的刷新动作
JMM提供了一些禁用缓存以及重排序的方法,如volatile,synchronized,final
重排序:执行的指令顺序
处理器的重排序:提供了读屏障,写屏障,全屏障来解决指令顺序
编译器的重排序:程序编写的指令在编译后指令可能会产生重排序来优化程序的执行性能
HappenBefore:前一个操作的结果对于后续操作是可见的,它表达多个线程之间对于内存的可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须腰存在happen-before关系
happen-before规则:
1:程序顺序规则:一个线程中的每个操作happens-before于该线程中的任意后续操作
2:volatile变量规则:对于volatile修饰的变量的写操作,一定happen-before后续对于volatile变量的读操作
3:传递性规则
4:start规则:线程A执行操作ThreadB.start(),那么线程A的ThreadB.start()操作happens-before线程B中的任意操作
5:join规则,如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before与线程A从ThreadB.join()操作返回
6:监视器锁的规则,对于一个锁的解锁happens-before与随后对这个锁的加锁
线程A执行完代码块后x=12(执行完自动释放锁),线程B进入代码块时,能看到线程A对x的写操作,线程B看到x=12