文章 https://mp.weixin.qq.com/s/DZkGRTan2qSzJoDAx7QJag 的学习笔记
相比synchronized关键字,volatile更轻量
- 它保证了变量在不同线程之间的可见性,但不保证原子性,即单线程写入不冲突
- 并且可以阻止编译时和运行时的指令重排
以上效果是利用CPU的内存屏障实现的
可见性与原子性
Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异
.
- 主内存(Main Memory)
主内存可以简单理解为计算机当中的内存,但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”
- 工作内存(Working Memory)
工作内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”
线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。变量值的传递只能经过主内存来进行
1 | static int s=0 |
执行流程如图
.
.
volatile能保证变量在不同线程之间的可见性(可以并发读写)
当一个线程修改了变量的值,新的值会立刻同步到主内存当中,即主内存→工作内存的值是最新值
但volatile并不能保证原子性(无法并发写),即会出现CAS问题
1 | 并发count++如下图 |
.
因此,什么时候适合用volatile呢?
1.确保只有单一的线程修改变量的值
2.变量不需要与其他的状态变量共同参与不变约束,解释如下
1 | volatile static int start = 3; |
这种情况下,start有可能先更新成6,造成了一瞬间 start == end,从而跳出while循环的可能性
指令重排
指令重排在多线程时可能会改变运行结果
内存屏障:屏障之前的操作被保证在屏障之后的操作之前执行,共分为四种类型:
- LoadLoad屏障:
抽象场景:Load1; LoadLoad; Load2
Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕
- StoreStore屏障:
抽象场景:Store1; StoreStore; Store2
Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见
- LoadStore屏障:
抽象场景:Load1; LoadStore; Store2
在Store2被写入前,保证Load1要读取的数据被读取完毕
- StoreLoad屏障:
抽象场景:Store1; StoreLoad; Load2
在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的
在一个变量被volatile修饰后,JVM会为我们做两件事
在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障
.