多线程|volatile的使用

发布时间 2023-08-29 18:29:31作者: 司丝思

一、内存可见性问题

先来看如下代码

class MyCounter{
public int flag = 0;
}
public class ThreadDemo22 {
public static void main(String[] args) {
MyCounter myCounter = new MyCounter();
Thread t1 = new Thread(() -> {
while(myCounter.flag == 0){

}
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("t1循环结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个数:");
myCounter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
执行结果如下:

 我们期望修改flag的值为1后,t1线程输出“t1线程结束循环”后结束循环,然而结果是修改flag的值为1之后,t1仍然没有结束循环,这种情况就叫做内存可见性问题。

上述的代码中,存在一个线程读,一个线程修改的情况,从汇编语言的角度来理解,读和修改分两步操作:

load,把内存中flag的值读到寄存器中;

cmd,把寄存器里flag的值与0比较,根据比较的结果来决定下一步怎么执行。                                                                                                                                                                                                                                     

由于CPU对寄存器的操作比对内存的操作快了几个数量级,因此,相较于cmd来说,load的速度很慢。load执行一次,cmd可能已经执行很多很多次了,在cmd执行的过程中,获取到的flag的值都是一样的,JVM有一个很大胆的设定,不再真正的执行load操作了,判定没人修改flag的值,干脆只load一次。

虽然JVM预判没有人会修改flag的值,实际上还是会修改flag的值,这时就需要程序员手动干预,给flag加上volatile关键字,加上volatile关键字表示被修饰的这个变量是“易变的”,也就是在告诉操作系统,每次都要重新进行load操作,因为这个变量的值随时都可能会变化,可不能只操作一次load。

我们用volatile修饰flag,如下。

 做了上述的调整后,看看代码的结果:

 此时代码执行结果就如我们期望的那样。

注意volatile只能修饰变量,volatile也不能保证原子性。