乐趣区

关于java:JDK成长记13深度好文你能从3个层面分析volatile底层原理么上

前几节你应该学习到了 Thread 和 ThreadLocal 的底层原理,在接下来的几节中,让咱们一起来摸索 volatile 底层原理吧!

不晓得你有没有这样的感触:有很多工程师都很难说分明 volatile 这个关键字的作用或者原理。比方有的人压根不晓得 volatile 的作用、利用场景;比方有的人也不晓得什么是有序性,可见性,原子性,比方有的人可能能说上来它的作用是什么“保障有序性,可见性,无奈保障原子性。”然而大多数人很难说分明为什么能保障有序性,可见性,不能保障原子性;比方在面试的时候,你常常被面试官问到 volatile 的时候,答复的支支吾吾的,没有一个清晰的思路,答不出一个称心的答案。诸如此类的场景有很多等等……

要想弄明确这些,可不是简略的事件。所以在接下来的《JDK 源码成长记 - 并发篇》中,就一步一步率领你来摸索 volatile 的神秘,来解决这些难堪的场景,能够纯熟使用和了解 volatile 关键字。

Hello Volatile

<div class=”output_wrapper” id=”output_wrapper_id” style=”width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: ‘Helvetica Neue’, Helvetica, ‘Hiragino Sans GB’, ‘Microsoft YaHei’, Arial, sans-serif;”><h3 id=”hdddd” style=”width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;”><span style=”font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;”>Hello Volatile</span></h3></div>

首先你要理解的第一点就是,什么时候应用 volatile。这里你要记住以下两点就能够了:

1、多个线程对同一个变量有读有写的时候

2、多个线程须要保障有序性和可见性的时候

让咱们别离来看看这两点:

volatile 第一个应用场景:多个线程对同一个变量有读有写的时候。你能够通过一个 Hello Volatile 的例子来了解这一点。

代码如下:

public class HelloVolatile {

   // 可见性举例
   private static volatile boolean shouldRunning = true;

   // 一个线程批改后,另一个线程无奈读到批改后的值,线程之间的内存数据不可见
   // private static boolean shouldRunning = true;
   

   public static void main(String[] args) {new Thread(()-> {System.out.println("读取到变量 shouldRunning="+HelloVolatile.shouldRunning);
       while(HelloVolatile.shouldRunning) { }
       System.out.println("运行完结,读取到变量 shouldRunning="+HelloVolatile.shouldRunning);
     }).start();

     

     new Thread(()-> {
       try {System.out.println("批改变量");
         Thread.sleep(1000);
         HelloVolatile.shouldRunning = false;
       } catch (InterruptedException e) {}}).start();} 
 }

下面的代码很显著能够看进去,两个线程。线程 1 在 while 循环中应用 shouldRunning 判断是否改跳出循环,线程 2 批改了 shouldRunning。这就是典型的一读一写的场景。

画张图让大家更好的了解下:

这种用法看上去很简略,然而其实在很多开源框架的底层,对线程执行管制都是通过这种形式管制的。等学完 volatile 之后,我会给大家举几个例子的。

volatile 第二个应用场景:须要保障有序性和可见性的时候。前面咱们会逐步钻研这两点。

下面的例子中,如果不加 volatile 润饰 shouldRunning 变量,线程 2 批改了值后,线程 1 是不可见的,也就不会跳出循环。

如果要想了解有序列性,这里给大家也给大家举一个经典的例子,在线程平安的单例(DLC-double check lock)的场景下,volatile 很重要的作用就是保障有序性。

还有一点要提到的是,volatile 既保证了有序性,也保障了可见性。并不是说 HelloVolatile 中没有有序性保障。

我给大家找了 SpringCloud Eureka 组件中的配置管理器创立,就是应用了 DCL 的单例。

代码如下:

public class ConfigurationManager { 

static volatile AbstractConfiguration instance = null;

public static AbstractConfiguration getConfigInstance() {if (instance == null) {synchronized (ConfigurationManager.class) {if (instance == null) {instance = getConfigInstance(Boolean.getBoolean(DynamicPropertyFactory.DISABLE_DEFAULT_CONFIG));
        }
      }
    }
    return instance;
 }
}

等学完 volatile 之后,咱们在回头看下这个 DCL 应用 volatile 保障有序性的。这里大家有个印象就行。

什么是有序性、可见性、原子性?

<div class=”output_wrapper” id=”output_wrapper_id” style=”width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: ‘Helvetica Neue’, Helvetica, ‘Hiragino Sans GB’, ‘Microsoft YaHei’, Arial, sans-serif;”><h3 id=”hdddd” style=”width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;”><span style=”font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;”> 什么是有序性、可见性、原子性?</span></h3></div>

前文提到了有序性、可见性、原子性。可能有人不太分明他们是什么意思。更不了解怎么保障的,原理是什么。

而你要想了解 volatile 如何保障有序性和可见性,首先须要明确有序性、可见性、原子性别离是什么。这里我先不深刻解说,先用一句话大白话简略给大家详情下。

  • 可见性,一句话讲就是多个线程中有读有写操作同一个变量的时候,线程间能够相互晓得,可见的意思。
  • 有序性,一句话讲就是因为代码执行程序可能被重排序,volatile 能够保障代码行数按程序执行。
  • 原子性,一句话讲就是当多个线程进行同时写同一个变量的时候,只能有一个线程进这一操作。

你可能看了下面三句话,还不是很明确,没关系,最初学习完 volatile 了,你能够回来再看看这三句话。

上面咱们 从浅入深 来摸索下这三点。次要档次有如下几个级别:

1、JVM 内存模型和 Java 内存模型(JMM) 层面

2、JVM 指令层面和 JVM 中的 C ++ 源码层面

3、CPU 缓存模型 + 硬件构造原理 +CPU 指令层面

JVM 内存构造和 JMM 的概念回顾

<div class=”output_wrapper” id=”output_wrapper_id” style=”width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: ‘Helvetica Neue’, Helvetica, ‘Hiragino Sans GB’, ‘Microsoft YaHei’, Arial, sans-serif;”><h3 id=”hdddd” style=”width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;”><span style=”font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;”>JVM 内存构造和 JMM 的概念回顾 </span></h3></div>

简略的讲,一句话:就是刷新主内存,强制过期其余线程的工作内存。

这句话中的主内存和工作内存是 Java 内存模型中的概念,要想了解 Java 内存模型(JMM),肯定要晓得 JVM 的内存构造(运行时内存区域)。上面通过几张图,让你回顾下 JVM 内存构造和 JMM 的概念。

首先回顾一下,JVM 的内存构造,如下图所示:

下面的这个图如果理解过 JVM 的同学,肯定很相熟了,不理解的也没有关系。这里简略介绍下,你就能够理解了:

JVM 的内存区域,或者说是运行时数据区,简略地来说分为堆和栈两种区域每个线程共享的区域除了堆内存,还有一个办法区的概念。不同 JVM 版本的办法区实现不同,JDK1.8 办法区的实现叫 MetaSpace 元数据空间,用于寄存加载到 JVM 内存中的类的根本信息和数据。堆内存就是创立的 Java 对象个别都会调配到堆内存,Heap 区域。这 2 个公共内存区域能够被所有的线程拜访到的。它们具体作用如下:

  • 堆(Heap):线程共享。所有的对象实例以及数组都要在堆上调配。回收器次要治理的对象。
  • 办法区(Method Area):线程共享。存储类信息、常量、动态变量、即时编译器编译后的代码。
  • 办法栈(JVM Stack):线程公有。存储局部变量表、操作栈、动静链接、办法进口,对象指针。
  • 本地办法栈(Native Method Stack):线程公有。为虚拟机应用到的 Native 办法服务。如 Java 应用 c 或者 c ++ 编写的接口服务时,代码在此区运行。
  • 程序计数器(Program Counter Register):线程公有。有些文章也翻译成 PC 寄存器(PC Register),同一个货色。它能够看作是以后线程所执行的字节码的行号指示器。指向下一条要执行的指令。

从色彩上能够看出,除了线程共享的内存区域,每个线程有本人的独有内存区域,比方程序计数器、本地办法栈,Java 办法虚拟机栈。这个是线程独有的内存区域,不会被其余线程所拜访到的。

上面给大家简略介绍下 JMM。它逻辑模型如下图所示:

下面的这个图能够看进去比拟形象,这是 因为 JMM 自身就是一种内存模型的形象,并不是理论存在的构造,而是有一种对应的具体实现和具体构造

大家都晓得很多事件在计算机层面,都会进行一层形象。比方网络的分层模型等等。而在 Java 中,精确说是 JVM 在内存这块的抽象概念是 JMM,即 Java 内存模型。

这个形象能够对应到具体 JVM 组件或者具体的硬件组件。对应关系能够了解为下图所示:

下面的提到的 JVM 内存构造,理论就是图中右边,示意和 JVM 的对应关系是,堆和元数据空间能够看做是主内存,Java 办法虚拟机栈、程序计数器等能够看做是本人的工作内存。

而对应左边的其实能够对应到 CPU 的 L1-L3 的缓存、高速缓存区、写缓冲器等能够看做 JMM 中每个线程的工作内存,而理论的物理内存这些能够看做是 JMM 中的主内存,线程共用的区域。

从 JMM 层面看,volatile 怎么保障可见性?

<div class=”output_wrapper” id=”output_wrapper_id” style=”width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: ‘Helvetica Neue’, Helvetica, ‘Hiragino Sans GB’, ‘Microsoft YaHei’, Arial, sans-serif;”><h3 id=”hdddd” style=”width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;”><span style=”font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;”> 从 JMM 层面看,volatile 怎么保障可见性?</span></h3></div>

回顾了 JVM 内存构造和 JMM 内存模型后,咱们来别离从这两个层面剖析 volatile 怎么保障的可见性。

首先是 JMM 层面。在 JMM 中,定义一些操作和规定来保障可见性。这里咱们深刻的讲 JMM 的常识,只是讲下咱们会用到的常识。

首先说下操作,JMM 规定了 8 中原子性操作,用来形容主内存和工作存在的操作动作和操作准则。

JMM 的指令

1) lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。

2) unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,开释后的变量才能够被其余线程锁定。

3) read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作应用

4) load(载入):作用于工作内存的变量,它把 read 操作从主内存中失去的变量值放入工作内存的变量正本中。

5) use(应用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个须要应用变量的值的字节码指令时将会执行这个操作。

6) assign(赋值):作用于工作内存的变量,它把一个从执行引擎接管到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

7) store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作。

8) write(写入):作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。

JMM 的指令应用规定

  • 不容许 read 和 load、store 和 write 操作之一独自呈现。即应用了 read 必须 load,应用了 store 必须 write
  • 不容许线程抛弃他最近的 assign 操作,即工作变量的数据扭转了之后,必须告知主存
  • 不容许一个线程将没有 assign 的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不容许工作内存间接应用一个未被初始化的变量。就是对变量施行 use、store 操作之前,必须通过 assign 和 load 操作
  • 一个变量同一时间只有一个线程能对其进行 lock。屡次 lock 后,必须执行雷同次数的 unlock 能力解锁
  • 如果对一个变量进行 lock 操作,会清空所有工作内存中此变量的值,在执行引擎应用这个变量前,必须从新 load 或 assign 操作初始化变量的值
  • 如果一个变量没有被 lock,就不能对其进行 unlock 操作。也不能 unlock 一个被其余线程锁住的变量
  • 对一个变量进行 unlock 操作之前,必须把此变量同步回主内存

下面的规定看上去很多,其实简略的来说,能够总结如下几句话:

必须按这个执行,不容许缺失或乱序 read–>load–>use 、assign–>store–>write; 一个个变量的 lock 操作,在同一时间内只容许一个线程反复执行屡次,并且只有执行雷同次数的 unlock 该变量能力被开释;开释锁 unlock 之前将最新数据写入主内存,进入锁 lock 之前将最新数据读入工作内存。

留神这 8 个操作,理论在 CPU 和 JVM 实现的指令层面并不齐全对应,前面咱们剖析到 JVM 指令的时候会看到。他们这些。

JMM 内存模型解释 Hello Volatile 如下图所示:

通过 JMM 的一些操作和准则,应用 volatile 就能保障不同线程的工作内存发送读写时候的变量可见性。

volatile 保障可见性的原理,还是之前总结的一句话:写入主内存数据时,刷新主内存值之后,强制过期其余线程的工作内存,底层是因为 lock、unlock 操作的准则导致的,其余线程读取变量的时候必须从新加载主内存的最新数据,从而保障了可见性。

好了,到这里你应该理解了 volatile 的根本作用和可见性的原理,理解了 JMM 和 JVM 和 volatile 之间的关系。

下一节咱们持续深入研究下,在 JVM 指令层面和 C ++ 代码层面,如何通过内存屏障、CPU 的 lock 前缀指令,保障可见性和有序性的。

本文由博客一文多发平台 OpenWrite 公布!

退出移动版