浅谈 Java 中的 volatile 作用(一):线程可见性

前言

volatile 在 Java 中有两个作用:

  • 线程可见性

    当一个线程对共享变量值做了修改,新值是能够及时的被其他线程获取到的。

  • 防止指令重排序

本篇博文结合代码谈谈 volatile 关键字的线程可见性。

概念

线程可见性是指当一个线程对共享变量值做了修改,新值是能够及时的被其他线程获取到的。

举个例子,假设有一共享变量 runFlag,该变量使用关键字 volatile 修饰:

volatile boolean runFlag = false;

另有 threadAthreadB 两个线程均能访问到变量 runFlag,当 threadArunFlag 值修改为 true 后,threadB 获取 runFlag 的值也会是 true,这便是线程可见性;

代码实验

实验一:不使用 volatile 关键字

VolatileRunnable.java

package com.shawearn.threads;

public class VolatileRunnable implements Runnable {

    private boolean runFlag;

    public VolatileRunnable(boolean runFlag) {
        this.runFlag = runFlag;
    }

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        // 当 runFlag 为 true 时进入循环,runFlag 为 false 时退出循环;
        System.out.println(threadName + " 开始运行...");
        while (runFlag) {
        }
        System.out.println(threadName + " 结束运行!");
    }

    public void setRunFlag(boolean runFlag) {
        this.runFlag = runFlag;
    }

    public boolean isRunFlag() {
        return runFlag;
    }

}

TestVolatile.java

package com.shawearn.threads;

public class TestVolatile {

    public static void main(String[] args) {
        // 1. 新建并启动子线程;
        VolatileRunnable runnable = new VolatileRunnable(true);
        Thread thread = new Thread(runnable, "【子线程】");
        thread.start();

        // 2. 主线程休眠 5 毫秒;
        try {
            Thread.sleep(5L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 3. 修改 runFlag 值为 false;
        System.out.println("【主线程】准备修改 " +
                "runnable.isRunFlag() = false");
        runnable.setRunFlag(false);
        System.out.println("【主线程】已成功修改 " +
                "runnable.isRunFlag() = " + runnable.isRunFlag());
    }
}

注意此时的 runFlag 未使用 volatile 关键字修饰。

运行程序,结果如下:

【子线程】 开始运行…
【主线程】准备修改 runnable.isRunFlag() = false
【主线程】已成功修改 runnable.isRunFlag() = false

主线程已经将 runFlag 的值修改为 false,然而子线程并没有退出循环,也就没有出现预期的输出:

【子线程】 结束运行!

实验二:使用 volatile 关键字

修改程序,将 VolatileRunnable 的 runFlag 用 volatile 关键字修饰:

private volatile boolean runFlag;

再次运行程序,结果如下:

【子线程】 开始运行…
【主线程】准备修改 runnable.isRunFlag() = false
【主线程】已成功修改 runnable.isRunFlag() = false
【子线程】 结束运行!

可以看到第二次实验程序顺利输出 【子线程】 结束运行!

两次实验证明使用 volatile 后,线程对变量的修改对其他线程是可见的。

深究一下,为什么?

不论是上面的实验一还是实验二,两个线程访问的都是同一个共享变量,按理说两个实验的结果应该是一致的——主线程修改 runFlagfalse,子线程退出循环并输出 【子线程】 结束运行!,然而两个实验的结果确实不一致的,为什么?

关于这个问题,需要从 Java 内存模型(Java Memory Model,简称JMM)说起,先看下面这张图:

浅谈 Java 中的 volatile 作用(一):线程可见性_第1张图片

Java 内存模型规定了如下几点

  • 所有的变量都存储在主内存中(此处的变量不包括局部变量与方法变量,因为局部变量与方法变量是线程私有的);
  • 每个线程都有自己的工作内存;
  • 线程的工作内存保存了被该线程的私有变量及该线程使用的变量的主内存副本;
  • 线程对自身使用的变量的读写必须在自己的工作内存中进行,线程不能直接读写主内存中的数据;
  • 不同线程之间无法直接访问对方工作内存中的数据;
  • 线程间变量值的传递均需要通过主内存进行;

对实验一的解释

结合 Java 内存模型的几条规定,可知道在实验一进行时,共享变量 runFlag 首先存放在主内存中,主线程与子线程分别从主内存中拷贝 runFlag 的副本到自己的工作内存中,由于线程对变量的读写必须在自己的工作内存中进行,在不对 runFlagvolatile 关键字修饰时,主线程对 runFlag 的修改不会立刻同步到主内存,而子线程也不会每次都从主内存刷新自己工作内存中的 runFlag 变量副本,导致主线程中对 runFlag 修改后的新值无法及时被子线程获取到,于是子线程迟迟不会结束 while 循环,也就看不到 【子线程】 结束运行! 的输出了。

对实验二的解释

实验二中使用 volatile 修饰了 runFlag 后,当主线程在工作内存中修改了 runFlag 值,会及时将工作内存中的 runFlag 同步到主内存中,而子线程每次访问 runFlag 时,会强制从主内存获取最新的 runFlag 到工作内存中,于是子线程得以结束 while 循环,也就看到 【子线程】 结束运行! 的输出了。

结论

使用 volatile 以后,每次修改 volatile 变量都会同步到主内存中,每次读取 volatile 变量的值都会强制从主内存读取最新的值。

你可能感兴趣的:(Java,笔记,多线程,java,并发编程,jvm,编程语言)