volatile 在Java日常开发中,尤其是并发编程中,是一个高频使用的关键字,它提供了一种轻量级的同步机制(常用于和synchronized比较),用来修饰成员变量。volatile 具有如下两大特性:
1. 保证内存可见性
2. 禁止指令重排序
volatile无法保证原子性。
我们先看一段代码:
- public class Test {
-
- public static boolean flag = false;
-
- public static void main(String[] args) throws Exception {
-
- Thread t1 = new Thread(new Runnable() {
- @Override
- public void run() {
- //子线程休眠1s,让主线程启动后再去修改flag
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- //TODO: 修改成员变量
- flag = true;
- System.out.println("子线程修改了flag = " + flag);
- }
- });
- t1.start();
-
- while (true) {
- if (flag) {
- System.out.println("主线程退出");
- break;
- }
- }
- }
- }
-

从运行结果中可以看到,子线程t1中将flag修改为了true,但是主线程并没有读到t1线程修改后的值,导致主线程一直无法退出。所以多线程情况下修改共享变量会出现某一个线程修改后对其他线程不可见。
在分析为什么不可见之前,我们先了解下Java内存模型JMM(和并发编程相关的模型)
JMM即为JAVA 内存模型(java memory model)。他是虚拟机规范定义的一种内存模型,因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型是一种标准化,屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。
Java内存模型(java memory model)描述了程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量的底层细节。 JMM 有如下规定:
本地内存和主线程的关系:

JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程
基于以上JMM的知识,我们就可以知道为什么我们的代码会出现不可见的问题.
顺便说一下:工作内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

上图中是一个大致的过程
那么细化后,我们用下面这张图来展示:

所有的共享变量都位于主内存中,每个线程有自己的本地内存(工作内存),而线程的工作内存是私有的,所以线程读写共享数据是必须经过主内存交换才可以,这就是产生不可见的原因。
根据上图可以知道,t1线程写回到主线程后,main线程无法感知,仍然使用自己工作内存中的变量。
在多线程下实现共享变量的可见性,有2种解决方案,一种是加锁(synchronized),另一种就是今天的主角:volatile
直接修改上面的代码:
- public class Test {
-
- public static boolean flag = false;
-
- public static void main(String[] args) throws Exception {
-
- Thread t1 = new Thread(new Runnable() {
- @Override
- public void run() {
- //子线程休眠1s
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- //TODO: 修改成员变量
- flag = true;
- System.out.println("子线程修改了flag = " + flag);
- }
- });
- t1.start();
-
- while (true) {
- //TODO:加锁
- synchronized (t1) {
- if (flag) {
- System.out.println("主线程退出");
- break;
- }
- }
- }
- }
- }
-
当某一个线程进入synchronized代码块前后,它的执行过程简单如下:
所以,当主线程进入while循环后,清空自己的工作内存,然后从主内存中拷贝flag到自己的工作内存中。注意:
synchronized要放到while内部,这样它就会不断的从主内存中读取flag, 直到读取到新值为true。
就是给共享变量flag添加volatile关键字,请看代码:
- public class Test {
-
- //TODO:给共享变量添加 volatile
- public static volatile boolean flag = false;
-
- public static void main(String[] args) throws Exception {
-
- Thread t1 = new Thread(new Runnable() {
- @Override
- public void run() {
- //子线程休眠1s
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- //TODO: 修改成员变量
- flag = true;
- System.out.println("子线程修改了flag = " + flag);
- }
- });
- t1.start();
-
- while (true) {
- if (flag) {
- System.out.println("主线程退出");
- break;
- }
- }
- }
- }
-

总结:volatile保证了多个线程对共享变量操作的可见性,也就是说某一个线程修改了共享变量并写回主存后,其他线程会立即看到修改后的新值。
如果还想探究某个线程写回主内存后,其他线程如何让自己的工作内存里的变量失效,以及如何感知主内存中的共享变量被修改了等这些细节内容,这就需要了解缓存一致性协议。
在学习缓存一致性协议之前,先首先简单看下计算机的大致结构

CPU的运算速度最快,内存的读写速度无法和其