volatile是Java语言提供的一个关键字,可用来修饰变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
volatile的使用与Java内存模型(JMM)有很大关系,不熟悉JMM的,请查看:Java并发编程之Java内存模型
注:下文中提到的本地内存,在其它书籍和文章中也叫做工作内存。
首先,我们引入一个例子说明volatile的作用。
package demo6;
public class Mythread {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("等待资源");
while (!flag) {
}
System.out.println("SUCCESS");
}).start();
//保证第一个线程限制性
Thread.sleep(1000);
new Thread(() -> {
System.out.println("准备资源");
flag = true;
System.out.println("准备完成");
}).start();
}
}
在上面的例子,开启了两个线程(线程A、B),线程A等待共享变量flag变为true后,继续执行;线程B负责将共享变量的值变为true。预期结果是线程B执行后,将flag变为true,然后线程A感知到flag值发生变化,从而跳出死循环。下面我们看下结果:
可以看出,结果和我们想象的不一样,线程B执行完成,线程A并没有发现flag值发生变化,因此没有跳出循环,一直在执行。
出现这种情况的原因就是,在JMM中有一块主内存,所有线程共享主内存。在线程执行期间 ,将主内存中共享的变量拷贝一份到工作内存中,而每个线程都有一个工作内存,并且是互相不可见的(不能相互访问)。因此线程B将flag的值修改后,线程A是不知道的。
为什么会出现这个问题呢?怎样解决这个问题呢?
这个问题就是并发编程可见性的问题,出现这个问题的原因是因为线程对变量的操作都是在本地内存中变量副本,主内存中的变量没有发生变化,所以其它内存感知不到。
可以使用volatile关键字,当然也可以用其他方式(synchronized、final、Lock、原子类型等),本篇重点介绍volatile关键字的使用。
再看一个例子
package demo6;
public class Mythread {
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("等待资源");
while (!flag) {
}
System.out.println("SUCCESS");
}).start();
//保证第一个线程限制性
Thread.sleep(1000);
new Thread(() -> {
System.out.println("准备资源");
flag = true;
System.out.println("准备完成");
}).start();
}
}
在本例中,共享变量flag的前面加上了volatile关键字,这次的执行结果又如何呢?
可以看出,在线程B将flag的值修改为true后,线程A感知到flag值的变化,跳出循环,执行结束。
接下来聊一聊volatile的工作原理
volatile的作用
1、本地内存中修改后的副本数据的缓存行立即回写到主内存(8、9)。
2、回写时触发总线嗅探机制,监听总线中CPU关心的flag变化,此时监听到线程A使用的CPU监听到flag已经变化,触发缓存一致性协议(MESI),它的中的本地内存中flag已过时,并将其置空。
所以,线程A再使用flag变量时就需要再次从主内存中读取,这时读到的是线程B修改后的值。
3、禁止指令重排序(类似内存屏障)。
volatile底层原理
汇编代码中加lock指令。
加volatile
未加volatile
volatile关键字总结:
1、作用:
实现并发编程可见性和有序性,未实现原子性。
2、底层实现原理:
在汇编代码中加lock指令。