多线程——volatile
多线程——volatile
定义
【在JVM 1.2之前,Java的内存模型实现总是从主存读取变量】
1.(适用于Java所有版本)volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。
2.(适用于Java5及其之后的版本)volatile 的读和写创建了一个happens-before关系,类似于申请和释放一个互斥锁。
作用
- 保证变量的可见性,但不保证原子性
- 禁止指令重排序
使用条件
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
(不具备原子性,不可在有原子性需求的场景下使用)
使用建议
在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
示例1
public class Demo1 {
private static volatile boolean flag = false;
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"读 flag="+flag);
// 线程1 读flag的值
new Thread(() -> {
while (true) {
if (flag) {
System.out.println("线程1 读 flag=" + flag);
System.exit(0);
}
}
}).start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 线程2 写flag的值
new Thread(() -> {
while (true) {
flag = true;
}
}).start();
}
}
结果
- main读 flag=false
线程1 读 flag=true
在使用了volatile关键字之后,线程1拿到了主内存中共享变量flag的值,结果符合预期。
示例2
public class Demo2 {
private static volatile int flag = 0;
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"读 flag="+flag);
// 线程1 读flag的值
new Thread(() -> {
while (true) {
if (flag == 10) {
System.out.println("线程1 读 flag=" + flag);// 注释0 打印语句会引起happens-before
System.exit(0);
}
}
}).start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 线程2 写flag的值
new Thread(() -> {
while (true) {
flag = 10; // 注释1 对flag的写操作依赖当前值
// flag++; // 注释2 对flag的写操作依赖当前值,当取消注释0处的打印语句会陷入死循环
}
}).start();
}
}
flag=10,结果:
- main读 flag=0
线程1 读 flag=10
flag++, 同时取消打印语句,结果:
-
main读 flag=0
之后陷入死循环
示例3
public class Demo3 {
private static volatile int flag = 0;
private static volatile int v = 0;
private static volatile int a = v + flag; // 该变量没有包含在具有其他变量的不变式中
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"读 flag="+flag);
// 线程1 读a的值
new Thread(() -> {
while (true) {
if(a == 10) {
System.exit(0);
}
}
}).start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 线程2 写v的值
new Thread(() -> {
while (true) {
v = 10;
}
}).start();
}
}
结果:
-
main读 flag=0
之后陷入死循环
示例2和示例3验证了volatile的使用条件,这是表明了volatile不具备原子性的特点。
原理实现
使用volatile后,发现汇编代码里对变量入栈操作时加了lock前缀,lock前缀的作用是禁止指令重排序,具体来说相当于加了一个叫内存屏障的东西,内存屏障会出现在共享变量的读写操作之间,来保证可见性。详细可见参考4
volatile会在部分读写操作间产生内存屏障导致不能重排序,下图时JMM针对编译器制定的重排序规则表。

下面展示volatile读写操作和普通操作相邻时出现的重排序情况
第一个操作volatile写a,第二个操作普通读a,可以看出发生了重排序

第一个操作volatile写a,第二个操作普通写b,可以看出发生了重排序

前一个普通写c,后一个个volatile读v,可以看出发生了重排序、

第一个操作普通读b,第二个volatile读v,可以看出发生了重排序

JMM为实现volatile内存语义,提供下面4种读写操作策略
- 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
- 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
- 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
- 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。


上面4种策略如果都同时采用的话时最保守的情况, 不过实际种内存屏障处理器的内存模型层面可以继续优化。例如X86处理不会对读读,读写,写写操作进行指令重排序。所以,在X86中,JMM不用考虑三种类型的内存屏障,仅需在volatile写后面加一个LoadStore内存屏障即可,不过执行LoadStore开销巨大。

浙公网安备 33010602011771号