1.作用
多线程环境下数据获取时的可见性,有序性(禁止指令重排),弱原子性。
可见性:就是一个线程修改了这个变量的值,就会刷新到主内存中,这个写操作会导致其他线程缓存中的值变为无效。
2.如何实现
也就是靠什么实现的这些功能:缓存一致性协议 和 内存屏障
缓存一致性协议,有很多种,常见的就是MESI(修改、独占、共享、无效)。
MESI的小例子:
两个核心共享某个数据,比如a = 0;(S状态),0 号核心修改了a的值,这时候 1 号核心中的 a 的值就无效了,但是 1 号核心如何知道它的 a 的值是无效的呢?是因为 0 号核心发了个广播,传的是无效化指令,核心 1 收到无效化指令之后,要将 a 的值设成无效,并返回一个无效化指令确认,表示已经改了,这时候 0 号核心才将新的 a 的值写到CPU cache中。
但是,传输无效化指令、收到无效化确认响应的过程中,核心 0 不能做任何事情,所以效率低,浪费了很多时间。为了不让核心 0 盲目等待,可以继续对 a 做别的事情,就用了store buffer和invalidate queue结构,核心 0 在收到无效化确认之前,先把 a 的新值写到store buffer中,并可以随意操作。无效化指令广播给核心 1 ,先存到 invalidate queue 中,等待核心 1 完成当前任务,再从队列里取无效化指令,并进行相应值的无效化操作,然后返回无效化确认响应。
但是,store buffer 和 invalidate queue 的存在又导致了缓存的不一致。
所以,用内存屏障
在写操作前,加入 StoreSotre 屏障(在我写之前,保证前面已经写完了),写操作后面加入 StoreLoad 屏障(在读之前,保证先让我写完)
在读操作前,加入 LoadLoad 屏障(在我读之前,保证前面先读完),读操作后面加入 LoadStore屏障(在后面写之前,保证先让我读完)
缓存一致性协议可以使volatile修饰的变量具有 可见性,内存屏障可以 禁止指令重排。
3.不能保证原子性
例子
比如有个共享变量 i ,两个线程都对这个共享变量 i 做 i++ 操作
而 i++ 操作的过程可以分为以下三个步骤:
1.获取变量 i 的值
2.变量 i 加一
3.写回到内存中
但如果线程 1 和 2 都进行到了第 2 步,然后先后写回内存,结果 i 的值是 1 ,而不是 2 。
由于缓存一致性协议MESI 通过Store Buffer 和 Invalidate Queue 两种结构进行加速,但这两种方式又会造成缓存不一致的问题。
所以有了内存屏障,针对 volatile 变量,JVM采用的内存屏障是:
1.volatile 修饰的变量的写操作:在写操作之前加入StoreStore屏障,写操作后加入 StoreLoad屏障
2.volatile 修饰的变量的读操作:在读操作之前加入LoadLoad屏障,读操作后加入 LoadStore屏障