volatile 关键字
import java.util.Scanner;
public class Demo2 {
private static int n = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(n == 0){
//啥都不写
}
System.out.println("t1 线程结束循环");
}, "t1");
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
n = scanner.nextInt();
}, "t2");
t1.start();
t2.start();
}
}
- 当我们输入一个非
0
的数,理应t1
中循环条件就不成立,将会打印“线程结束循环”,但实际上输入1
后,t1
没有任何动静- 我们通过
jconsole
可以看到t1
线程仍是持续工作的
- 上述问题的原因,就是“内存可见性问题”
内存可见性问题
层次 | 空间 | 速度 | 成本 | 数据 | |
---|---|---|---|---|---|
CPU 寄存器 | 小 | 快 | 高 | 掉电后丢失 | |
内存 | 中等 | 中等 | 中等 | 掉电后丢失 | |
硬盘 | 大 | 慢 | 低 | 掉电后不丢失 |
while(n == 0) {
}
- 上面代码中的这个操作,循环会执行非常多次,每次循环,都要执行一个
n == 0
这样的判定
- 从内存读取数据到寄存器中(读取内存,相比之下,这个操作的速度非常慢)
- 通过类似
cmp
指令,比较寄存器和0
的值(这个指令执行速度非常快)- 此时
JVM
执行这个代码的过程的时候,发现:每次执行循环操作的开销非常大,并且每次执行的结果都是一样的- 并且
JVM
根被没有意识到,用户可能在未来会修改n
,于是JVM
就做了一个大胆的操作——直接把这个操作给优化掉了
- 每次循环,不会重新读取内存中的数据,而是直接读取
寄存器/cache
中的数据(缓存的结果)- 当
JVM
做出上述决定之后,此时意味着循环的开销大幅度降低了,但当用户修改n
的时候,内存中的n
已经改变了,但是由于t1
线程每次循环,不会真的读内存,所以感知不到n
的改变- 内存中的
n
的改变,对于线程t1
来说是“不可见的”,这样就引起了 bug
- 内存可见性问题本质上是
编译器/JVM
对代码进行优化的时候,优化出了 bug - 如果代码是单线程的,
编译器/JVM
的代码优化一般都是非常准确的,优化之后,不会影响到逻辑 - 但是代码如果是多线程的,
编译器/JVM
的代码优化就可能出现误判(编译器/JVM
的 bug),导致不该优化的地方也给优化了,于是就造成了内存可见性问题
[!quote] 编译器问啥要做优化?
- 有些程序员写出来的代码太低效了,为了能降低程序员的门槛,即使你的代码写的一般,最终执行也不会落下风
- 因此一些主流的编译器,都会好引入优化机制(优化手段是多种多样的)
- 优化就是编译器自动调整你写的代码,保持原有逻辑不变的前提下,提高代码的执行效率
- 代码优化的效果是非常明显的
- 若一个服务器在开启优化的时候启动时间为 10 min,那么在不开启优化的时候,启动时间可能会在 30 min+
若在 while 循环中加入一个 sleep 操作:
while(n == 0) {
Thread.sleep(10);
}
System.out.println("t1 线程结束循环");
//在输入1后,成功输出:"t1 线程结束循环"
- 说明加入
sleep
之后,刚才谈到的针对读取n
内存数据的优化操作不再进行了- 因为和读取内存相比,
sleep
的开销更大,远远超过了读取内存,就算把读取内存的操作优化掉,也没有意义,杯水车薪
volatile 关键字的用法
volatile
关键字修饰一个变量,提示编译器说这个变量是“易变”的- 编译器进行上述优化的前提,是编译器认为,针对这个变量的频繁读取,结果都是固定的
- 使用
volatile
关键字修饰变量之后,编译器就会禁止上述的优化,确保每次循环都是从内存中重新读取数据
private static volatile int n = 0;
- 编译器的开发者,知道这个场景中可能出现误判,于是就把权限交给程序员,让程序员能够部分的干预到优化的进行
- 引入
volatile
的时候,编译器生成这个代码的时候,就会给这个变量的读取操作附近生成一些特殊的指令,称为“内存屏障”,后续JVM
执行到这些特殊指令,就知道不能进行上述优化了
volatile 只是解决内存可见性问题,不能解决原子性问题,如果两个线程针对同一个变量进行修改(count++),volatile 也无能为力
[!quote] 网络上“内存可见性”问题:
- 工作内存(其实就是 CPU 的寄存器和 cache)
- 主内存
- 整个 Java 程序持有这个主内存,每个 Java 程序又有一份自己的工作内存
- 像上述例子中的内存变量 n,本身是在主内存中,在 t1 和 t2 线程工作的过程中,就会把主内存的数据拷贝到>工作内存中
- t2 如果修改了 n,先修改工作内存,再写回到主内存中。t1 读取 n 的时候,则是从主内存加载到工作内存,接下来的判定都是依照工作内存的值来进行判定的。此时 t2 修改了主内存,对于 t1 的工作内存未产生影响,从而出现了上述内存可见性问题