你真的了解 volatile 关键字吗?(2)

class ThreadTwo implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(2000L);
                System.out.println("flag 状态改变");
                flag = true;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

public static void main(String[] args) {
        VolatileTest testVolatile = new VolatileTest();
        Thread thread1 = new Thread(testVolatile.new ThreadOne());
        Thread thread2 = new Thread(testVolatile.new ThreadTwo());
        thread1.start();
        thread2.start();
    }
}

上述结果有可能在线程 2 执行完 flag = true 之后,并不能保证线程 1 中的 while 能立即停止循环,原因在于 flag 状态首先是在线程 2 的私有内存中改变的,刷新到主存的时机不固定,而且线程 1 读取 flag 的值也是在自己的私有内存中,而线程 1 的私有内存中 flag 仍未 false,这样就有可能导致线程仍然会继续 while 循环。运行结果如下:

执行操作
执行操作
执行操作
flag 状态改变
任务停止

避免上述不可预知问题的发生就是用 volatile 关键字修饰 flag,volatile 修饰的共享变量可以保证修改的值会在操作后立即更新到主存里面,当有其他线程需要操作该变量时,不是从私有内存中读取,而是强制从主存中读取新值。即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

比如下面的代码

int i = 0;           
boolean flag = false;
i = 1;        // 1
flag = true;  // 2

代码定义了一个 int 型变量,定义了一个 boolean 类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句 1 是在语句 2 前面的,那么 JVM 在真正执行这段代码的时候会保证语句 1 一定会在语句 2 前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(InstructionReorder)。

语句 1 和语句 2 谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句 2 先执行而语句 1 后执行。

但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

int a = 10;    // 1
int r = 2;      // 2
a = a + 3;      // 3
r = a * a;      // 4

这段代码执行的顺序可能是 1->2->3->4 或者是 2->1->3->4,但是 3 和 4 的执行顺序是不会变的,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令 Instruction2 必须用到 Instruction1 的结果,那么处理器会保证 Instruction1 会在 Instruction2 之前执行。

虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

// 线程1
String config = initConfig();    // 1
boolean inited = true;          // 2
 
// 线程2
while(!inited){
      sleep();
}
doSomeThingWithConfig(config);

上面代码中,由于语句 1 和语句 2 没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程 1 执行过程中先执行语句 2,而此时线程 2 会以为初始化工作已经完成,那么就会跳出 while 循环,去执行 doSomeThingWithConfig(config) 方法,而此时 config 并没有被初始化,就会导致程序出错。

从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

那么 volatile 关键字修饰的变量禁止重排序的含义是:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/e9eb1db51192fc09e066c4aeca17a875.html