Java多线程 - volatile

系列章节

  1. java多线程-基础
  2. Java多线程 - Synchronized
  3. java多线程 - wait/notify

volatile关键字用于在多线程处理时,JMM(Java内存模型)工作内存的数据未能及时刷新到主内存中,导致其它线程执行异常。

退不出的循环

 private static boolean RUN = true;
 ​
 // 线程t1中的`while`根据条件变量`RUN`来结束循环
 public static void main(String[] args) {
     Thread t = new Thread(() -> {
         while (RUN) {
             // ...
         }
     }, "t1");
     t.start();
     sleep(1);
     // 线程t1不会如预想的停下来
     RUN = false; 
 }
复制代码

运行上述代码后,我们预想的情况是条件变量改变后,线程t1会退出循环,但实际并没有。那么我们来分析下。

image.png

  1. 初始t1线程读取RUN的值到工作内存,因其while循环需要频繁读取RUN的值,所以JIT会对其进行优化,将RUN缓存至t1线程工作内存中
  2. Main线程改变了RUN的值,并同步至主内存,但t1线程还是读取工作内存的值,最终导致其循环不能退出

image.png

volatile解决方案

volatile可以用来修饰成员变量和静态成员变量,它使线程必须到主内存中获取值

 // 添加`valatile`关键字
 private static volatile boolean RUN = true;
 ​
 // 这时程序可以正常结束
 public static void main(String[] args) {
     Thread t = new Thread(() -> {
         while (RUN) {
             // ...
         }
     }, "t1");
     t.start();
     sleep(1);
     RUN = false;
 }
复制代码

可见性

上述例子体现的就是volatile的可见性,它保证在多个线程之间,一个线程对volatile变量的修改对另一个线程可见。

可见性和原子性

原子性是保证多线程中执行某段代码块不会发生指令交错,但可见性只能保证获取最新值,不能阻止指令交错。

synchronized 可以保证代码块的原子性,也可以保证代码块内变量的可见性。但其是重量级操作,性能相对更低。

有序性

有序性牵扯到重排序,我们先了解下重排序。

为什么要有重排序?

简单理解,深入层面很复杂。深入请看为什么需用指令重排序

系统层面:CPU 计算的时候访问值,如果经常利用到寄存器中已有的值就不用去内存读取了。

Java层面:

 public static void main(String[] args) {
     String a = "a" + "b";// 1
     String b = "ab"; // 2
     System.out.println(a == b);
 }
复制代码

上述代码经过编译器优化其实就是一个ab字符串在字符串常量池中,那么重排序优化后如果2先执行,先有了ab字符串,那么1其实就不会创建ab两个字符串了。

重排序导致的问题

在单线程环境中,重排序不会有问题;但多线程环境中,重排序可能会导致意想不到的结果。

 // 计数
 static int i = 1;
 // 定义四个静态变量
 private static int x, y, a, b = 0;
 ​
 public static void main(String[] args) throws InterruptedException {
     while (true) {
         Thread t1 = new Thread(() -> {
             a = 1; // 1
             x = b; // 2
         });
         Thread t2 = new Thread(() -> {
             b = 1; // 3
             y = a; // 4
         });
         t1.start();
         t2.start();
         t1.join();
         t2.join();
         // 打印输出
         String result = "第" + i++ + "次执行x=" + x + ", y=" + y;
         System.out.println(result);
         if (x == 0 && y == 0) {
             break;
         }
         // 修改完后重新赋值
         x = 0;
         y = 0;
         a = 0;
         b = 0;
     }
 }
复制代码

如上代码,运行后我们看预期有几种输出情况:

  1. 同步执行,结果 x=0,y=1
  2. 指令交错,结果 x=1,y=1 或 x=1,y=0

但实际还会有 x=0,y=0,如下图所示(执行时间可能会比较长)

image.png

这就是指令重排序的结果,如图:

image.png

2先于1执行,4先于3执行。

volatile原理

volatile的底层实现是内存屏障机制,如下:

屏障类型 指令示意
LoadLoad Load1; LoadLoad; Load2; 确保 Load1读取数据时在 Load2 及后续所有读取操作之前
StoreStore Store1; StoreStore; Store2; 确保 Store1 写入数据时刷新到主内存,并且在 Store2 及后续写入操作之前
LoadStore Load1; LoadStore; Store2; 确保 Load1 读取数据时在 Store2 及后续所有写操作之前
StoreLoad Store1; StoreLoad; Load2; 确保 Store1 写入数据时刷新到主内存,并且在 Load2 及后续读操作之前

如下是在openjdk8根路径/hotspot/src/share/vm/interpreter路径下的bytecodeInterpreter.cpp文件中,处理putstaticputfield指令的代码片段,其中就对volatile变量写入后加了StoreLoad屏障。

image.png

猜你喜欢

转载自juejin.im/post/7079790020495671327