前面我们讲到过了线程的一些性质,以及Synchronizd关键字的的用法以及原理(保整理原子性,可见性)。今天介绍的也是个关键字(volatile - 保证了可见性)
可见性:可见性就是当一个线程执行的时候,每次的读取都是最新的值,不存在值已经更新了,但是取出的值还是原来的值(否则那只能说明更新失败等)。但在多线程的情况下,这种情况就是很有可能发生,例如:线程A 更新了主内存的某个值,但是此时线程B刚好要获取这个值,因此这个时候线程B获取的值很有可能就是历史的值了,因为这个时候线程A 修改后的值还未被更新到主内存上,这就是所谓的可见性
那么应该如何保证可见性的呢?基于GP-Mic老师的课程我这边也从硬件层面和软件层面来说说我对这个一块的理解吧!
硬件层面 (CPU ,I/O,内存等) CPU (高速缓存) --> 正式因为有高速缓存的存在所以会引发一个问题 :缓存一致性问题。为什么这么说? 在读个CPU的情况下,每次的更新都会触发主内的更新,但是这个更新并非实时的。引出了总线锁和缓存锁
总线锁和缓存锁
总线锁:当有多个线程同时访问主内存的数据时,这个时候总线会发出一个Lock的指令,只允许一个处理器进行修改,其他都只能等待。显然这个是不行,这个会严重影响到效率,本来并行,结果总线锁了,变成了串行了。
缓存锁 :可以看做是一种优化,多个CPU同时缓存了主内存的数据,当有修改的时候,这个时候就会用上缓存锁了,因此并不会加在总线上. 先修改本内部的值,然后通过缓存一致性协议(MESI)来保证可见性
MESI
M(Modify) : 表示共享数据只缓存在当前CPU缓存中,并且是被修改状态,也就是缓存的数据和主内
存中的数据不一致
E(Exclusive) :表示缓存的独占状态,数据只缓存在当前CPU缓存中,并且没有被修改
S(Shared): 表示数据可能被多个CPU缓存,并且各个缓存中的数据和主内存数据一致
I(Invalid) :表示缓存已经失效
这个时候我们发现,在更新操作的时候,CPU0需要通知其他CPU这个数据已失效,而这个过程是处于阻塞中,因此这个也是不可以取的。
这个时候又引入了storeBuffers,它的作用就是不让CPU0处于等待中,由原来的同步变成了异步。那么这个时候又会引入一个新的问题,什么问题呢? 因为storeBuffer的引入会导致指令重排序的问题,什么是指令重排序呢?
为了解决这个指令重排序的问题,通过内存屏障来解决这个问题。
前面的操作对后面的操作是可见的
软件层面
通过JMM来定义了一个规范,简单来说,JMM定义了共享内存中多线程程序读写操作的行为规范.JMM是一个抽象模型,它是建立在不同的操作系统和硬件层面之上对问题进行了统一的抽象,然后再Java层面提供了一些高级指令,让用户选择在合适的时候去引入这些高级指令来解决可见性问题
通过提供的这些高级指令来保证可见性,以及重排序问题(比如今天要说的 Volatile)
Volatile 的原理 (Happens-Before模型) 前面的操作对于后面是可见的
Happens-Before模型
1、程序的顺序规则 (即在不影响到最终结果的情况下,保证程序的有序性)
2、传递性规则 (a happens-bdfore b ,b happens-before c --> a happens-before c)
3、volatile 变量规则
volatile int a = 1;
4、synchronized规则
5、start规则
Thread thread = new Thread();
flag = fasle;
thread.start();
6、join 规则
thread .start();
thread .join();