Java并发-JMM的8大原子操作及并发3之volatile关键字可见性

摘要

我们之前讲解了JMM模型,以及其引入的必要行,以及JMM与JVM内存模型的比较和JMM与硬件内存结构的对应关系。

思维导图

本节主要讲解思维导图如下:

内容

1、JMM的8大原子操作

1、lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
2、unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定。
3、read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以 便随后的load动作使用。
4、load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的 变量副本中。
5、use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚 拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
6、assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
7、store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随 后的write操作使用。
8、write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

注意:
1、如果需要把变量总主内存赋值给工作内存:read和load必须是连续;read只是把主内存的变量值从主内存加载到工作内存中,而load是真正把工作内存的值放到工作内存的变量副本中。
2、如果需要把变量从工作内存同步回主内存;就需要执行顺序执行store跟write操作。store作用于工作内存,将工作内存变量值加载到主内存中,write是将主内存里面的值放入主内存的变量中。

代码实例:


public class VolatileTest2 {
     static boolean  flag = false;

     public void refresh(){
         this.flag = true;
         String threadName = Thread.currentThread().getName();
         System.out.println("线程: "+threadName+" 修改共享变量flag为"+flag);
     }
     public void load(){
         String threadName = Thread.currentThread().getName();
         while (!flag){

         }
         System.out.println("线程: "+threadName+" 嗅探到flag状态的改变"+" flag:"+flag);
     }
     public static void main(String[] args) {
         /**
          * 创建两个线程
          */
         VolatileTest2 obj = new VolatileTest2();
         Thread thread1 = new Thread(() -> {
             obj.refresh();
         }, "thread1");
         Thread thread2 = new Thread(() -> {
             obj.load();
         }, "thread2");

         thread2.start();
         try {
             /**
              * 确保我们线程2先执行
              */
              Thread.sleep(2000);
         }catch (Exception e){
             e.printStackTrace();
         }
         thread1.start();
     }
}

我们发现上面代码数据结果为:

线程: thread1 修改共享变量flag为true

并且主线程不会退出,说明有用户线程在runnable运行中,说明线程2一直在运行,也说明线程2获取的变量值先从主内存read到工作内存,然后load给线程2里面工作内存里面变量,然后线程2一直是从自己工作内存获取数据,并且线程2是while的空转,抢占cpu时间多,所以一直不退出。

2、基于8大原子操作程序数据加载回写流程

8大原子操作是怎样做的?变量是如何读取、如何赋值的?

Java并发-JMM的8大原子操作及并发3之volatile关键字可见性_第1张图片

上面是线程2执行后的结果;所以线程2先读取到flag=false;所以先不会退出。

接着线程1会执行修改flag的操作。将flag修改成true;
第1步:read变量到
第2步: load到工作内存里去;
第3步: use传递给执行引擎做赋值操作。
第4步: 将修改后的值assign到工作内存;这个值会从false变成true;

那么工作内存里面的新值flag=true会立马同步到主内存里面去吗?
更新后的新值不会立马同步到我们的主内存里面去,他需要等待一定的时机。时机到了之后会同步到我们的主内存中去;

同步的时候也需要分为执行两步骤:store和write操作。
但是更新到主内存为true之后,为什么我们的线程2为什么没有感知到了;原因线程2在while进行循环判断的时候,一直判断的是我们线程2自己的工作内存里面的值。执行引擎一直判断;判断的值一直是工作内存里面的值。

然后我们修改代码如下;在while循环判断里面加一个i++的话,那么我们的线程2能不能及时感知到flag变化的值呢?
Java并发-JMM的8大原子操作及并发3之volatile关键字可见性_第2张图片
因为工作内存中已经存在这个值的话,就不会从主内存去加载。

我们修改代码如下:线程3去读取主内存flag的值,因为线程3是从主内存加载的线程1已经写入的值,此时这个值是flag=true;所以ok。
Java并发-JMM的8大原子操作及并发3之volatile关键字可见性_第3张图片

然后我们加上一个同步代码快之后的效果呢?
Java并发-JMM的8大原子操作及并发3之volatile关键字可见性_第4张图片

通过上面分析,我们的线程2已经感知到了flag数据的变化。 这是什么原因呢?这里很多人都搞不明白,这里有一个很大的坑:加了同步快之后,我们的线程2就能够读取到我们线程1修改的数据,这个是为什么呢?

原因:之前我们说了,之前没有加同步代码块之前,我们程序指令一直在循环/或者一直在做i++操作。循环是空的,可以理解为其近似在自旋跑;此时此线程对cpu的使用权限是特别高的;别的线程压根就抢不到cpu的时间片。我们加了同步快之后,我们此时线程会产生阻塞(cpu的使用权限被别的线程抢去了)。产生阻塞之后会发生线程上下文切换。如下:

Java并发-JMM的8大原子操作及并发3之volatile关键字可见性_第5张图片

2、可见性

可见性: 一个线程对某个共享主内存变量进行修改之后,其他与此共享变量相关的线程会立马感知到这个数据的更改。其他线程可以看到某个线程修改后的值。
之前代码我们发现,我们两个线程一个线程1修改掉flag的值之后,线程2是load读取不到写的值的,那么为了保证线程将简单标记为变量的可见性。我们最简单的方式是使用volatile关键字进行修改这个多线程共享的变量。

public class VolatileTest2 {
     static volatile boolean  flag = false;
     public void refresh(){
         this.flag = true;
         String threadName = Thread.currentThread().getName();
         System.out.println("线程: "+threadName+" 修改共享变量flag为"+flag);
     }
     public void load(){
         String threadName = Thread.currentThread().getName();
         while (!flag){

         }
         System.out.println("线程: "+threadName+" 嗅探到flag状态的改变"+" flag:"+flag);
     }
     public static void main(String[] args) {
         /**
          * 创建两个线程
          */
         VolatileTest2 obj = new VolatileTest2();
         Thread thread1 = new Thread(() -> {
             obj.refresh();
         }, "thread1");
         Thread thread2 = new Thread(() -> {
             obj.load();
         }, "thread2");

         thread2.start();
         try {
             /**
              * 确保我们线程2先执行
              */
              Thread.sleep(2000);
         }catch (Exception e){
             e.printStackTrace();
         }
         thread1.start();
     }
}

输出结果如下:

线程: thread1 修改共享变量flag为true
线程: thread2 嗅探到flag状态的改变 flag:true

volatile底层原理
volatile是Java虚拟机提供的轻量级的同步机制
volatile语义有如下两个作用:

  • 可见性:保证被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了被volatile修饰的共享变量的值,新值总是可以被其他线程立即得知。
  • 有序性:禁止指令重排序优化:内存屏障。

volatile缓存可见性实现原理:

  • JMM内存交互层面:volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步到主内存,使用时必须从主内存刷新,由此保证volatile可见性。
  • 底层实现:通过汇编lock前缀指令,他会锁定变量缓存行区域并写会主内存,这个操作成为“缓存锁定”,缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器缓存失效。

汇编代码查看:

  • -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp

Java并发-JMM的8大原子操作及并发3之volatile关键字可见性_第6张图片

缓存一致性原理再次剖析:
线程1跟线程2都已经将flag=false的值加载到各自的工作内存,此时flag的状态都是S状态(共享状态),此时线程2将修改flag的值为true时候,其状态变成了M状态,这个时候线程1所在的cpu会嗅探到flag值修改让后将flag对应的缓存行状态设置为I(无效状态),然后我们线程1需要使用的时候由于值无效,需要重新加载,此时需要重新加载的话,需要线程2将修改的值添加到主内存,然后线程1才能够加载到正确的值。

Java内存模型内存交互操作:
把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read个load操作,如果把变量从工作内存中同步到主内存中,就需要按照顺序地执行 store个write操作。但是Java内存模型只要求上述操作必须按照顺序执行,而没有保证必须是连续执行的。

以上是顺序性而不是连贯的,注意read跟load必须成对出现;store跟write必须成对出现。

你可能感兴趣的