废话不说,直接上代码。
private Boolean flag = false;
private void test() throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread1 start.");
while(!flag) {
}
System.out.println("thread1 end");
}
});
thread1.start();
Thread.sleep(1000);
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2 start");
flag = true;
System.out.println("thread2 end");
}
});
thread2.start();
}
上面的这段代码,执行结果是:
thread1 start.
thread2 start.
thread2 end
那么thread2里面对flag进行赋值,为什么thread1不会停止呢?
在jvm中,每个线程中都有一个相对应的栈,在thread1的run栈帧中,flag变量的值并不会随着thread2的run栈帧中的flag变量的变化而变化。
换句话说,这个flag变量,在内存中存在三个。即堆、run1栈帧、run2栈帧。
那么在flag变量的前面加上volatile关键字,就可以把thread1和thread2中的flag变量保持同步。
在volatile关键字声明的变量上,栈帧对静态变量表中的变量进行赋值时,会同时写入堆中变量。在这之前,会同时通知其他栈帧a,清除静态变量表中同一变量,在其他栈帧a使用时,会去堆中再次寻找该变量。这种方式也称作 “JMM 缓存一致性协议”。
所以volatile的第一个作用就是“线程可见性”。
在volatile声明的变量下,操作数栈中write该变量时,同步到堆中之前,会对其进行lock,也就是其他线程是无法read到变量的。在同步后解锁该变量。
volatile第二个作用是禁止指令重排。
当我们使用单例模式的时候,通常都是双重检查模式。thread1进来创建对象的时候,如果只初始化了一个对象引用,还没有赋值。thread2再来获取对象,检查不为null时,就会直接返回该对象,但是对象中的变量值还没有被赋值。
所以使用volatile可以避免出现这种问题。