0%

volatile关键字

文章 https://mp.weixin.qq.com/s/DZkGRTan2qSzJoDAx7QJag 的学习笔记

相比synchronized关键字,volatile更轻量

  1. 它保证了变量在不同线程之间的可见性,但不保证原子性,即单线程写入不冲突
  2. 并且可以阻止编译时和运行时的指令重排

以上效果是利用CPU的内存屏障实现的

可见性与原子性

Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异

.image-20220307112001941

  • 主内存(Main Memory)

主内存可以简单理解为计算机当中的内存,但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”

  • 工作内存(Working Memory)

工作内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”

线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。变量值的传递只能经过主内存来进行

1
2
static int s=0
s=3//线程A

执行流程如图

.image-20220307112620774

.image-20220307112638130

volatile能保证变量在不同线程之间的可见性(可以并发读写)

当一个线程修改了变量的值,新的值会立刻同步到主内存当中,即主内存→工作内存的值是最新值

但volatile并不能保证原子性(无法并发写),即会出现CAS问题

1
并发count++如下图   

.image-20220307141443857

因此,什么时候适合用volatile呢?

1.确保只有单一的线程修改变量的值

2.变量不需要与其他的状态变量共同参与不变约束,解释如下

1
2
3
4
5
6
7
8
9
10
11
volatile static int start = 3;
volatile static int end = 6;

线程A:
while (start < end){
//do something
}

线程B:
start+=3;
end+=3;

这种情况下,start有可能先更新成6,造成了一瞬间 start == end,从而跳出while循环的可能性

指令重排

指令重排在多线程时可能会改变运行结果

内存屏障:屏障之前的操作被保证在屏障之后的操作之前执行,共分为四种类型:

  1. LoadLoad屏障:

抽象场景:Load1; LoadLoad; Load2

Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕

  1. StoreStore屏障:

抽象场景:Store1; StoreStore; Store2

Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见

  1. LoadStore屏障:

抽象场景:Load1; LoadStore; Store2

在Store2被写入前,保证Load1要读取的数据被读取完毕

  1. StoreLoad屏障:

抽象场景:Store1; StoreLoad; Load2

在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的

在一个变量被volatile修饰后,JVM会为我们做两件事

  • 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障

  • 在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障

.image-20220307144753536