Java关键字volatile是如何保证内存可见性以及防止指令重排序的?

本文首发地址 https://www.dgjava.com/archives/javavolatile01

volatile在Java并发编程中常用于保持内存可见性和防止指令重排序
这句话很简单,看起来很好理解,但似乎又不好理解,所以我们干脆通过代码来说明。

/**
 * 

深入理解volatile关键字

* * @Author: 地瓜(yangxw) * @Date: 2020/4/8 12:55 AM */ public class VolatileTest { final static int MXA = 5; static int init_value = 0; public static void main(String[] args) { new Thread(() -> { int local_value = init_value; while (local_value < MXA) { if (init_value != local_value) { System.out.printf("init_value 更新为[%d]\n", init_value); local_value = init_value; } } }, "reader").start(); new Thread(() -> { int local_value = init_value; while (local_value < MXA) { ++local_value; System.out.printf("init_value 变为[%d]\n", local_value); init_value = local_value; try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }, "update").start(); } }

这是一段很简单的代码,main方法中启动两个线程。第一个线程比较 init_valuelocal_value 的值,如果不相等,打印最新的 init_value 的值,并更新。第二个线程则对 local_value 做自加操作,并赋值给 init_value 。理想状态下两个线程同时运行,应该输出类似 :

  • init_value 更新为1
  • init_value 变为1

如此交替递增。然而实际效果如下图:
Java关键字volatile是如何保证内存可见性以及防止指令重排序的?_第1张图片

可以看到,第一个线程的输入代码没有如预期的那样实现,那就意味着在第一个线程的while循环中,local_value 首次赋值后,init_value 的值一直没变。实际上真的没有变吗?基于以上代码,我将第一个线程做一个改造,在循环中休眠200ms,并输出最新的local_value和init_value,代码如下:

        new Thread(() -> {
            int local_value = init_value;
            while (local_value < MXA) {
                try {
                    TimeUnit.MILLISECONDS.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.printf("local_value=[%d],init_value=[%d]\n", local_value, init_value);
                if (init_value != local_value) {
                    System.out.printf("init_value 更新为[%d]\n", init_value);
                    local_value = init_value;
                }
            }
        }, "reader").start();

实际输出结果为:
Java关键字volatile是如何保证内存可见性以及防止指令重排序的?_第2张图片

看到这个结果,似乎有些明了。在第一个线程中,并不是init_value没有发生变化,只是线程执行太快,没能及时读取到init_value的最新值。

在一开始说到过,volatile在Java并发编程中常用于保持内存可见性和防止指令重排序。那我们不妨在init_value前加上volatile关键字,看看是否能在第一个线程中实时读取到init_value在内存中的最新值,我们把休眠的代码去掉,只保正常留输出。代码如下:

public class VolatileTest {
    final static int MXA = 5;
    volatile static int init_value = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            int local_value = init_value;
            while (local_value < MXA) {
                if (init_value != local_value) {
                    System.out.printf("init_value 更新为[%d]\n", init_value);
                    local_value = init_value;
                }
            }
        }, "reader").start();

        new Thread(() -> {
            int local_value = init_value;
            while (local_value < MXA) {
                ++local_value;
                System.out.printf("init_value 变为[%d]\n", local_value);
                init_value = local_value;
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "update").start();
    }
}

再来看看运行结果
Java关键字volatile是如何保证内存可见性以及防止指令重排序的?_第3张图片

通过这个图,那么volatile在Java并发编程中常用于保持内存可见性就很好理解了。同时也可以结果,为什么volatile关键字在高并发场景下,为何那么好用了。

源码地址

你可能感兴趣的:(#,Java)