系列章节
序
volatile
关键字用于在多线程处理时,JMM(Java内存模型)工作内存的数据未能及时刷新到主内存中,导致其它线程执行异常。
退不出的循环
private static boolean RUN = true;
// 线程t1中的`while`根据条件变量`RUN`来结束循环
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (RUN) {
// ...
}
}, "t1");
t.start();
sleep(1);
// 线程t1不会如预想的停下来
RUN = false;
}
复制代码
运行上述代码后,我们预想的情况是条件变量改变后,线程t1会退出循环,但实际并没有。那么我们来分析下。
- 初始
t1
线程读取RUN
的值到工作内存,因其while循环需要频繁读取RUN
的值,所以JIT
会对其进行优化,将RUN
缓存至t1
线程工作内存中 Main
线程改变了RUN
的值,并同步至主内存,但t1
线程还是读取工作内存的值,最终导致其循环不能退出
volatile解决方案
volatile
可以用来修饰成员变量和静态成员变量,它使线程必须到主内存中获取值
// 添加`valatile`关键字
private static volatile boolean RUN = true;
// 这时程序可以正常结束
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (RUN) {
// ...
}
}, "t1");
t.start();
sleep(1);
RUN = false;
}
复制代码
可见性
上述例子体现的就是volatile
的可见性,它保证在多个线程之间,一个线程对volatile
变量的修改对另一个线程可见。
可见性和原子性
原子性是保证多线程中执行某段代码块不会发生指令交错,但可见性只能保证获取最新值,不能阻止指令交错。
synchronized
可以保证代码块的原子性,也可以保证代码块内变量的可见性。但其是重量级操作,性能相对更低。
有序性
有序性牵扯到重排序,我们先了解下重排序。
为什么要有重排序?
简单理解,深入层面很复杂。深入请看为什么需用指令重排序
系统层面:CPU 计算的时候访问值,如果经常利用到寄存器中已有的值就不用去内存读取了。
Java层面:
public static void main(String[] args) {
String a = "a" + "b";// 1
String b = "ab"; // 2
System.out.println(a == b);
}
复制代码
上述代码经过编译器优化其实就是一个ab
字符串在字符串常量池中,那么重排序优化后如果2
先执行,先有了ab
字符串,那么1
其实就不会创建a
和b
两个字符串了。
重排序导致的问题
在单线程环境中,重排序不会有问题;但多线程环境中,重排序可能会导致意想不到的结果。
// 计数
static int i = 1;
// 定义四个静态变量
private static int x, y, a, b = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
Thread t1 = new Thread(() -> {
a = 1; // 1
x = b; // 2
});
Thread t2 = new Thread(() -> {
b = 1; // 3
y = a; // 4
});
t1.start();
t2.start();
t1.join();
t2.join();
// 打印输出
String result = "第" + i++ + "次执行x=" + x + ", y=" + y;
System.out.println(result);
if (x == 0 && y == 0) {
break;
}
// 修改完后重新赋值
x = 0;
y = 0;
a = 0;
b = 0;
}
}
复制代码
如上代码,运行后我们看预期有几种输出情况:
- 同步执行,结果 x=0,y=1
- 指令交错,结果 x=1,y=1 或 x=1,y=0
但实际还会有 x=0,y=0,如下图所示(执行时间可能会比较长)
这就是指令重排序的结果,如图:
2
先于1
执行,4
先于3
执行。
volatile原理
volatile
的底层实现是内存屏障机制,如下:
屏障类型 | 指令示意 | |
---|---|---|
LoadLoad | Load1; LoadLoad; Load2; | 确保 Load1读取数据时在 Load2 及后续所有读取操作之前 |
StoreStore | Store1; StoreStore; Store2; | 确保 Store1 写入数据时刷新到主内存,并且在 Store2 及后续写入操作之前 |
LoadStore | Load1; LoadStore; Store2; | 确保 Load1 读取数据时在 Store2 及后续所有写操作之前 |
StoreLoad | Store1; StoreLoad; Load2; | 确保 Store1 写入数据时刷新到主内存,并且在 Load2 及后续读操作之前 |
如下是在openjdk8
根路径/hotspot/src/share/vm/interpreter
路径下的bytecodeInterpreter.cpp
文件中,处理putstatic
和putfield
指令的代码片段,其中就对volatile
变量写入后加了StoreLoad
屏障。