三丶多线程的进阶二(volatile)
1.volatile关键字的使用
volatile关键字,用在我们的变量前可以保证我们的变量的可见性(一个线程操作这个变量的时候,可以对其他的线程可见)。
2.volatile的原理
通过添加这个关键字的,可以是我们的数据在底层执行的指令的时候加了一个lock标志。
3.LOCK命令在底层的执行逻辑
1.首先需要了解一下我们的线程是如何内存交互的
这里可以看到,每个线程都会又有一个自己的高速缓存区,他们还会共享一个缓存区域,最后才会去修改主存。
2.lock锁其实就是告诉我们计算机底层去加锁,从而保证数据的安全性。但是锁有两种状态一个总线锁,另一个缓存锁。
总线锁:
总线锁就是锁住总线,当一个线程获取之后,其他线程都不能访问这个内存区域,知道当前线程释放锁,其他线程才能访问。(缺点就是消耗比较大)
缓存锁:
目的就是控制锁的缓存力度,通过保证缓存的一致性来解决。
两者优先使用缓存锁,但是有以下几种情况不能使用缓存锁:
①当前数据不能被缓存的当前线程。
②处理器不支持缓存锁定。
4.缓存一致性协议
常见的一致性协议有 MSI、MESI、MOSI 等。最常见的就是 MESI 协议。
4.1、MESI协议
M(Modify) 表示共享数据只缓存在当前 CPU 缓存中, 并且是被修改状态,也就是缓存的数据和主内存中的数据不一致。
E(Exclusive) 表示缓存的独占状态,数据只缓存在当前 CPU 缓存中,并且没有被修改。
S(Shared) 表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致。
I(Invalid) 表示缓存已经失效。
在 MESI 协议中,每个缓存的缓存控制器不仅知道自己的 读写操作,而且也监听其它 Cache 的读写操作。
对于 MESI 协议,从 CPU 读写角度来说会遵循以下原则:
CPU 读请求:缓存处于 M、E、S 状态都可以被读取,I 状 态 CPU 只能从主存中读取数据。
CPU 写请求:缓存处于 M、E 状态才可以被写。对于 S 状 态的写,需要将其他 CPU 中缓存行置变成I。
比如我们刚刚说的到这种情况:就会变成下面这样:
但是这样虽然保证了缓存的一致性,但是由于在线程1通知线程2之间,并且线程2没有回应成功之前,线程1一致处于阻塞状态。所以就引入一个store buffers 进行缓存,类似于一个异步的操作。
这样又带来一个新的问题,这样就会进行指令重排序。然后通过内存屏障解决了指令重排序。
内存屏障的三种类型:
JMM
JMM提供了多线程处理共享数据内存安全的规范。他是针对不同的操作系统的统一规范,类似jvm。
JMM是通过总线锁和缓存锁来进行底层 的处理,而对于指令重排序的问题,JMM在java层面上提供了volatile等关键字来保证禁止高速缓存,禁止指令重排序来处理。
总结
从计算机命令底层触发,通过总线锁和缓存锁保证数据的一致性,但是总线锁消耗过大,然后出现了缓存锁,缓存锁避免的线程等待引入了 store buffers进行异步,从而导致命令重排序,然后通过内存屏障来处理,保证数据的一致性。而JMM就为JAVA层面提供了volatile关键字来保证数据的可见性。