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

38次阅读

共计 4194 个字符,预计需要花费 11 分钟才能阅读完成。

摘要

咱们之前解说了 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 大原子操作是怎么做的?变量是如何读取、如何赋值的?

下面是线程 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 变动的值呢?

因为工作内存中曾经存在这个值的话,就不会从主内存去加载。

咱们批改代码如下:线程 3 去读取主内存 flag 的值,因为线程 3 是从主内存加载的线程 1 曾经写入的值,此时这个值是 flag=true; 所以 ok。

而后咱们加上一个同步代码快之后的成果呢?

通过下面剖析,咱们的线程 2 曾经感知到了 flag 数据的变动。这是什么起因呢?这里很多人都搞不明确,这里有一个很大的坑:加了同步快之后,咱们的线程 2 就可能读取到咱们线程 1 批改的数据,这个是为什么呢?

起因:之前咱们说了,之前没有加同步代码块之前,咱们程序指令始终在循环 / 或者始终在做 i ++ 操作。循环是空的,能够了解为其近似在自旋跑;此时此线程对 cpu 的应用权限是特地高的;别的线程压根就抢不到 cpu 的工夫片。咱们加了同步快之后,咱们此时线程会产生阻塞(cpu 的应用权限被别的线程抢去了)。产生阻塞之后会产生线程上下文切换。如下:

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

缓存一致性原理再次分析:
线程 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 必须成对呈现。

正文完
 0