前言
在提到多线程高并发时volatile是绕不过去的一个关键字,曾经对volatile也是一知半解,在踩过一些坑之后对volatile有了更深入的理解,在此对volatile做一个总结。
主要用途
-
变量可见性
说到多线程环境下变量的可见性问题,首先得提一下Java的内存模型JMM
JMM
在JMM模型中内存分为主内存和工作内存,每个线程拥有自己独立的工作内存,主内存、工作内存互相之间不影响。比如说我们在主线程中定义了一个变量a,那么变量a是存储在主内存中的,其它线程会拷贝一份变量a的副本存储在各自的工作内存中,当修改了主线程中变量a的值时其它线程是感知不到的,那么此时volatile就派上用场了。当给变量a添加volatile修饰之后,变量a的值发生变化时其它线程就会感知到。下面通过举个例子来说明这一点:
public class VolatileTest {
public static Boolean flag = false;
public static void main(String args[]) throws InterruptedException {
Thread t = new Thread(() -> {
while (!flag) {
System.out.println("flag is false");
}
if (flag == true) {
System.out.println("now flag is true");
}
});
t.start();
Thread.sleep(1000);
flag = true;
t.join();
}
}
当运行这个程序时大家猜猜看结果会是什么呢?会打印出"now flag is true"吗?不会打印这句话,运行结果是程序会陷入死循环中不断打印"flag is false",明明在主线程中设置了flag变量为true,为什么在线程t中flag还是false呢?这就是上面讲的主内存和工作内存中的变量是互不影响的,也就是主线程中的flag变量值被修改为true时,线程t中的flag变量不会被修改依然是false。如果给flag变量增加volatile修饰那程序还会死循环吗?答案是不会死循环了。
public class VolatileTest {
public volatile static Boolean flag = false; // 添加volatile将flag变量设置为其它线程可见
public static void main(String args[]) throws InterruptedException {
Thread t = new Thread(() -> {
while (!flag) {
System.out.println("flag is false");
}
if (flag == true) {
System.out.println("now flag is true");
}
});
t.start();
Thread.sleep(1000);
flag = true;
t.join();
}
}
主线程中的flag变量增加volatile修饰之后,线程t能够感应到flag变量值的变化,当主线程中将flag值修改为true时,在线程t中flag值也被修改为true。
- 防止指令重排
什么是指令重排呢?简单来讲就是程序中几条语句的执行顺序被打乱了
int a = 1; // 语句1
int b = 2; // 语句2
int c = a * a; // 语句3
int d = b + b; // 语句4
比如这4条语句在单线程场景下执行顺序是1->2->3->4,但是到了多线程场景就不一定是按照1234这个顺序执行了,执行顺序可能是1->3->2->4、2->1->3->4,但不会出现的场景是语句3在语句1之前执行、语句4在语句2之前执行,这是因为语句3对语句1有依赖(同样的语句4对语句2有依赖)。如果想阻止乱序执行语句这种行为可以给变量添加volatile修饰,比如给变量a、b都添加上volatile修饰,那么即使是在多线程场景这4条语句一定会顺序执行。不过要说明一下的是这种乱序执行发生的概率还是比较低的,一旦发生的话程序可能会发生不可预期的结果。我曾经遇到这样一起事件,事后经过反复压测好多次才复现出一次,我当时的做法是在每条语句前后都加上日志,日志中会打印时间戳,通过分析语句执行的时间点才看出来是语句执行乱序了,最后加上volatile之后就没再出现过这种情况了。
