volatile关键字深入解析 JMM与内存屏障
1.volatile关键字的作用?
2.volatile如何实现的?(底层原理)
3.volatile平时怎么用?
1.volatile关键字的作用?
老生常谈 张口就来 volatile保证可见性,不保证原子性,防止指令重排
这里简单过一下
1.1.保证可见性?
当线程安全问题(多个线程操作同一共享资源)出现时,对共享资源加volatile关键字,可以保证每个线程都能读到最新的值.
经典JMM模型,volatile就是能保证共享变量a每一次发生变化,都能通知到每一个线程,再次把主内存的最新值刷入各自的工作内存
(这里的主内存就是jvm主存,这里的各线程工作内存就是cpu高速缓存,一般分为L1,L2,L3三级缓存)
可见性达到的效果,白话讲就是 线程1 线程2都比较关注a这个变量,如果变量a加了volatile关键字,当线程3修改了变量a,线程1 2会立刻拿到最新的值,抛弃原来的值
1.2.不保证原子性?
原子性,原子性,一系列操作作为一个原子操作,要么成功要么失败。
经典i++问题, i = 0,如果线程1 线程2 同时操作i++时,可能会造成i = 1 而不是i = 2,丢了一次++被覆盖掉,
造成原因: i++在底层是三条指令, 从主内存获取i,执行i+1,刷回主内存,
当线程1执行刚执行完第3步时,线程2刚好执行完第二步,线程2虽然能根据可见性读到变量i最新的值已经改成1了,但是刚才是在0的基础上+1的,且并没有回滚操作,说啥都晚了,只得把i=1刷回主存
(如何保证原子性呢,简单CAS操作,每次要刷主内存之前我先判断现在的i是不是中途被人改过,如果有回滚重来一遍3步)
1.3.防止指令重排?
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段,现代 CPU中指令的执行次序不一定按顺序执行,没有相关性的指令可以打乱次序执行,以充分利用 CPU的指令流水线,提高执行速度(具体参见指令重排)。
具体例子可以参考一下之前的章节,单例模式懒汉式double-check情况下,为什么还要加volatile修饰静态变量
2.volatile如何实现的?
2.1.如何实现的可见性?
主要利用了缓存一致性原理
有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。
volatile的内存可见性是如果一个线程修改了共享变量,那么该共享变量会立刻刷新到主存中。同时,会通知另外一个持有该共享变量的线程,告诉它这个共享变量已经修改了,不要再使用你工作内存中的变量值了,快去主内存中重新获取吧。
映射到硬件层就是,通过volatile保证了cpu缓存中永远可以读到主存变量最新的值
2.2.如何实现的防止指令重排(什么是内存屏障)?
JMM被内存屏障指令分为了4类(Load表示读,store表示写)
LoadLoad Barriers:在两个读指令之间插入一个“LoadLoad”的内存屏障,确保Load1的数据装载,先于Load2的数据装载。
StoreStore Barriers:在两个写指令之间插入一个“StoreStore”的内存屏障。确保Store1的数据先刷新到主内存,并且对其数据可见。Store1的写数据先于Store2的写数据。
LoadStore Barriers:在读和写指令之间加一个“LoadStore”屏障,确保Load1的数据装载先于Store2的写数据。
toreLoad Barriers:在写和读之间加一个“StoreLoad”屏障,确保Store1的数据写入并且刷新到内存先于Load2。
“StoreLoad”会使该屏障之前所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。执行“StoreLoad”屏障的开销比较昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中
为了实现volatile的内存语义,编译器在生成字节码的时候,JMM采取保守策略会向指令序列中插入内存屏障来禁止特定类型的处理器重排序。
1.在每个volatile写操作前面插入一个StoreStore屏障。
2.在每个volatile写操作后面插入一个StoreLoad屏障。
3.在每个volatile读操作后面插入一个LoadLoad屏障。
4.在每个volatile读操作后面插入一个LoadStore屏障。
volatile禁止指令重排序就是使用了内存屏障作为保证来实现的。
3.volatile平时怎么用?
要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
由于volatile不能保证原子性,所以像i++这种操作,依赖前一次结果的 不应直接用volatile,当然如果同一时间只有单个写线程,那倒是无所谓
3.1.适用于作独立标志
由于 volatile 简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile。
3.2.适用于防止指令重排场景
见于经典的单例模式double-check后,由于指令重排,得到的单例是null
3.3.适用于轻量级读写锁
读 通过volatile保证读到最新值
写 加重量级锁保证线程安全
见于ConcurrentHashMap jdk1.7之前实现读写分离方式
over