我正在参与「启航方案」

前言

1、你知道什么是 Java 内存模型 JMM 吗

2、JMM 与 Volatile 它们两个之间的联系?

3、JMM 有哪些特性 or 它的三大特性是什么?

4、为什么要有 JMM,它为什么呈现?效果和功能是什么?

5、happens-before 先行产生准则你有了解过吗?

一、JMM 入门

1.1 概述

硬件体系中存在多级的缓存,越往寄存器,则运算速度越快。

CPU 的运转并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作就会形成不共同的问题。

JUC(7) : JMM & Volatile | 死磕内存模型

因为每个人用的操作体系不同,JVM 标准中企图界说一种 Java 内存模型(JMM)来屏蔽各种硬件和操作体系的内存拜访差异。以完结让 Java 程序在各种平台下都能达到共同的内存拜访效果。所以,咱们需求知道 JMM 。

JUC(7) : JMM & Volatile | 死磕内存模型

处理器出来的数据放入高速缓存中。咱们可以在主内存和高速缓存中拟定一个 缓存共同性协议。

1.2 界说效果

JMM(Java 内存模型 Java Memory Model)自身是一种笼统的概念,并不真实存在。它仅仅描述的是一组约好或标准,经过这组标准界说了程序中(尤其是多线程)各个变量的读写拜访办法并决定一个线程对同享变量的写入何时对别的一个线程可见。

关键技术点都是环绕多线程的原子性、可见性和有序性展开的

能干么呢?

1、经过 JMM 来完结线程和主内存之间的笼统联系

2、屏蔽各个硬件平台操作体系的内存拜访差异,以完结让 Java 程序在各种平台下都能达到共同的 内存拜访效果。

1.3 多线程对变量的读写过程

  • 主内存:线程的同享数据区域,首要存储的是 Java 实例目标,一切线程创立的实例目标都存放在主内存中
  • 作业内存:每个线程对应一个私有作业内存,首要存储当时办法的一切本地变量信息(同享数据的副本),也可以了解为本地内存

JUC(7) : JMM & Volatile | 死磕内存模型

JMM界说了线程和主内存之间的笼统联系

① 线程之间的同享变量存储在主内存中(从硬件视点来说便是内存条)

② 每个线程都有一个私有的本地作业内存,本地作业内存中存储了该线程用来读/写同享变量的副本(从硬件视点来说便是CPU的缓存,比如寄存器、L1、L2、L3缓存等)

③ 线程对同享变量一切的操作都有必要先在线程自己的作业内存中进行后写回主内存,不能直接从主内存中读写(不能越级)

④ 不同线程之间也无法直接拜访其他线程的作业内存的变量,线程间变量值的传递需求经过主内存来进行(同级不能彼此拜访)

总线窥视

总线窥视是缓存中的共同性操控器监督或窥视总线事务的一种方案,其目标是在分布式同享内存体系中保护缓存共同性。当主内存的数据被修正后,需求将一切有该数据副本的作业内存的数据改动,这个数据改动告知可以经过总线窥视来完结,一切的窥视者都在监督总线上的每一个事务,假如一个修正同享数据的事务呈现在总线上,一切的窥视者都会查看自己的副本是否有相同数据副本,若有则修正。

1.4 八种内存交互

JUC(7) : JMM & Volatile | 死磕内存模型

  • read:效果于主内存,将变量的值从主内存传输到作业内存,主内存到作业内存;
  • load:效果于作业内存,将 read 从主内存传输的变量值放入作业内存变量副本中,即数据加载
  • use:效果于作业内存,将作业内存变量副本的值传递给履行引擎,每逢 JVM 遇到需求该变量的字节码指令时会履行该操作。
  • assign:效果于作业内存,将从履行引擎接收到的值赋值给作业内存变量,每逢 JVM 遇到一个给变量赋值字节码指令时会履行该操作;
  • store:效果域作业内存,将赋值完毕的作业变量的值写回给主内存;
  • write:效果于主内存,将 store 传输过来的变量值赋值给主内存的变量;

因为上述6条不能确保多条指令组合的原子性,没有大面积加锁

  • lock:效果于主内存:将一个变量标记为一个线程独占的状况,仅仅写时分加锁,就仅仅锁了写变量的过程。
  • unlock:效果于主内存,把一个处于确定状况的变量释放,然后才干被其他线程占用。

二、JMM 三大特性

并发编程Bug 的源头:可见性、原子性和有序性问题。

2.1 可见性

2.1.1 事例

修正变量的值。

public class Demo1 {
    private  boolean flag =true;
    private int count = 0;
    public void update(){
        flag =false;
        System.out.println(Thread.currentThread().getName()+"修正flag");
    }
    public void load()   {
        while (flag){
            count++;
        }
        System.out.println("完毕线程,count = "+count);
    }
    public static void main(String[] args) throws InterruptedException {
        Demo1 demo1 = new Demo1();
        new Thread(()->{
             demo1.load();
        },"线程A").start();
        // 等候三秒,看别的一个线程修正后,能否进入循环
        Thread.sleep(3000);
        new Thread(()->{
            demo1.update();
        },"线程B").start();
    }
}

操控台输出,线程B修正后,线程 A 无法中止,即无法读取到变量 flag 被其他线程修正了。

2.1.2 可见性

当一个线程修正了某一个同享变量的值,其他线程是否可以当即知道该改动。Java 内存模型是经过在变量修正后将新值同步回主内存,在变量读取前从主内存改写变量值这种依靠主内存作为传递媒介的办法来完结可见性的。

JMM 规矩了一切的变量都存储在主内存中。

JUC(7) : JMM & Volatile | 死磕内存模型

每个线程都从主内存中读取同享变量的副本,然后修正后提交给主内存,确实像 git。

体系主内存同享变量数据修正被写入的时机是不确定的,多线程并发下很或许呈现 ”脏读“,所以每个线程都有自己的作业内存,线程自己的作业内存中保存了该线程运用到的变量的主内存副本复制。线程间变量值的传递均需求经过主内存来完结。

2.1.3 怎样确保可见性

  • 经过 volatile 关键字确保可见性
    • java 中被 volatile 润饰的变量,在转变汇编指令后会添加一个 lock 前缀,lock 前缀的指令在多核处理器下做两件事:
      • 1、将当时处理器缓存行的数据写回体系内存
      • 2、写回内存后,其他cpu缓存了该内存地址的数据无效
  • 经过内存屏障确保可见性
  • 经过 synchronized 关键字确保可见性
    • 加锁线程会获得锁,清空作业内存,从主存复制同享变量最新的值到作业内存,源码运用内存屏障完结可见性
  • 经过 lock 确保可见性
  • 经过 final 关键字确保可见性
    • final 润饰的量不可变

总结:处理方案可以分为两类:

  • 线程上下文切换:让出cpu时刻片,线程切换回导致当时线程本地内存失效;
  • 内存屏障:jvm 层面的 storeLoad 内存屏障(下节解说)

2.2 有序性

关于一个线程的履行代码而言,咱们总是习惯性认为代码的履行总是从上到下,有序履行。但为了供给功能,编译器和处理器通常会对指令序列进行从头排序。

指令重排可以确保串行语义共同,但没有责任确保多线程间的语义也共同。即或许产生”脏读“,简单说,两行以上不相干的代码在履行的时分有或许先履行的不是第一条,不见得是从上到下的次第履行,履行次第会被优化。

JVM 能依据处理器特性(CPU多级缓存体系、多核处理器等)恰当的对机器指令进行重排序,使机器指令能更符合 CPU 的履行特性,最大极限的发挥机器功能。

JUC(7) : JMM & Volatile | 死磕内存模型

单线程环境里边确保程序最终履行成果和代码次第履行的成果共同。处理器在进行重排序时有必要要考虑指令之间的数据依靠性,多线程环境中线程替换履行,因为编译器优化重排的存在,两个线程中运用的变量能否确保共同性是无法确定的,成果无法预测。

在某些情况下可以制止指令重排。

怎样确保有序性?

  • 经过 volatile 关键字确保有序性
  • 经过内存屏障确保有序性
  • 经过 synchronized 关键字确保有序性
  • 经过 Lock 确保有序性

2.3 原子性

指一个操作是不可打断的,即多线程环境下,操作不能被其他线程搅扰。例如关于一个静态变量 i =0,线程 A 对它赋值 1,线程 B 对它赋值 -1,那么它要么是 1 要么是 -1,这便是原子性。

怎样确保原子性:

  • 经过 synchronized 关键字确保原子性
  • 经过Lock确保原子性
  • 经过 CAS 确保有序性

三、happens-before(多线程先行产生准则)

在JMM 中,假如一个操作履行的成果需求对另一个操作可见性或者代码重排序,那么这两个操作之间有必要存在 happens-before 联系。(逻辑上的先后联系)

JUC(7) : JMM & Volatile | 死磕内存模型

3.1 入门事例

JUC(7) : JMM & Volatile | 死磕内存模型

由这个入门事例可知,第一条句子和第二条句子不能进行排序,不然了能会呈现过错。

happens-before 准则十分重要,它是判别数据是否存在竞赛、线程是否安全的首要依据,依托这个准则,咱们处理在并发环境下两操作之间是否或许存在抵触的一切问题。

3.2 先行产生准则说明

假如 Java 内存模型中一切的有序性都仅靠 volatile 和 synchronized 来完结,那么很多操作都将变得十分啰嗦。

可是咱们在编写 Java 并发代码的时分并没有察觉到这一点。

咱们没有时时、处处、次次,添加 Volatile 和 synchronized 来完结程序,这是因为 Java 语言中 JMM 准则下有一个”先行产生“(Happens-Before)的准则限制和规矩,给你定好了规矩。

这个准则十分重要:

  • 它是判别数据是否存在竞赛,线程是否安全的十分有用的手法。
  • 依靠这个准则,咱们可以经过几条简单规矩一揽子处理并发环境下两个操作之间是否或许存在抵触的一切问题,而不需求陷入 Java 内存模型苦涩难明的底层编译原理之中。

3.3 总准则(面试答)

  • 假如一个操作 happens-before 另一个操作,那么第一个操作的履行成果将对第二个操作 可见 ,并且第一个操作的履行次第排在第二个操作之前。
  • 两个操作之间存在 happens-before 联系,并不意味着一定要依照 happens-before 准则拟定的 次第 来履行。假如重排序之后的履行成果与依照 happens-before 联系来履行的成果共同,那么这种重排序并不不合法

3.4 8条

3.4.1 次第规矩

一个线程内,依照代码次第,写在前面的操作先行产生于写在后边的操作;

前一个操作的成果可以被后续的操作获取。说白了前面一个操作将变量 X 赋值1,那么后边一个操作必定能知道 X 现已变成 1.

3.4.2 确定规矩

一个unLock操作先行产生于后边((这儿的“后边”是指时刻上的先后))对同一个锁的lock操作;

public class HappenBeforeDemo
{
    static Object objectLock = new Object();
    public static void main(String[] args) throws InterruptedException
    {
        //关于同一把锁objectLock,threadA一定先unlock同一把锁后B才干获得该锁,   A 先行产生于B
        synchronized (objectLock)
        {
        }
    }
}

3.4.3 volatile 变量规矩

对一个volatile变量的写操作先行产生于后边对这个变量的读操作,前面的写对后边的读是可见的,这儿的“后边”同样是指时刻上的先后。

3.4.4 传递规矩

假如操作A先行产生于操作B,而操作B又先行产生于操作C,则可以得出操作A先行产生于操作C;

3.4.5 线程发动规矩

Thread目标的start()办法先行产生于此线程的每一个动作

3.4.6 线程中止规矩

对线程interrupt()办法的调用先行产生于被中止线程的代码检测到中止事情的产生;

可以经过Thread.interrupted()检测到是否产生中止,也便是说你要先调用 interrupt()办法设置中止标志位,我才干监测到中止发送。

3.4.7 线程中止规矩

线程中的一切操作都先行产生于对此线程的中止检测,咱们可以经过Thread::join()办法是否完毕、 Thread::isAlive()的返回值等手法检测线程是否现已中止履行。

3.4.8 目标完结规矩

一个目标的初始化完结(构造函数履行完毕)先行产生于它的finalize()办法的开端目标没有完结初始化之前,是不能调用finalized()办法的

3.5 事例分析

假设存在线程A和B,线程A先(时刻上的先后)调用了setValue(1),然后线程B调用了同一个目标的getValue(),那么线程B收到的返回值 是什么?

JUC(7) : JMM & Volatile | 死磕内存模型

咱们就这段简单的代码一次分析happens-before的规矩(规矩5、6、7、8 可以疏忽,因为他们和这段代码毫无联系):

1 因为两个办法是由不同的线程调用,不在同一个线程中,所以必定不满足程序次第规矩;

2 两个办法都没有运用锁,所以不满足确定规矩;

3 变量不是用volatile润饰的,所以volatile变量规矩不满足;

4 传递规矩必定不满足;

所以咱们无法经过happens-before准则推导出线程A happens-before线程B,尽管可以承认在时刻上线程A优先于线程B指定,但便是无法承认线程B获得的成果是什么,所以这段代码不是线程安全的。那么怎样修复这段代码呢?

  • 把getter/setter办法都界说为synchronized办法
  • 把value界说为volatile变量,因为setter办法对value的修正不依靠value的原值,满足volatile关键字运用场景

3.6 总结

在Java 语言里边,Happens-Before 的语义本质上是一种可见性。

A Happens-Before 意味着 A 产生过的事情对 B 来说是可见的,不管 A 事情和 B 事情是否产生在同一个线程里。

JMM 的规划分为两部分:

  • 一部分是面向咱们程序员供给的,也便是 happens-before 规矩,它通俗易懂的向咱们程序员阐述了强内存模型,咱们只要了解 happens-before 规矩,就可以编写并发安全的程序了.
  • 另一部分是针对 JVM 完结的,为了尽或许少的对编译器和处理器做约束从而进步功能,JMM 在不影响程序履行成果的前提下对其不做要求,即允许优化重排序。咱们只需求重视前者就好了,也便是了解 happens-before 规矩即可,其他繁杂的内容有 JMM 标准结合操作体系给咱们搞定,咱们只写好代码即可。

四、Volatile

4.1 Volatile 关键字确保的可见性

volatile 字面意思是易变的,不稳定的,在 Java 中是个关键字,作为一个类型润饰符,运用办法如下

static volatile int i=0;

其意图是告知咱们,该变量是极有或许多变的,不能随意变动目标指令,并确保该变量上操作的原子性。

  • volatile 润饰的变量有可见性,其含义是变量被修正后,应用程序范围内的一切线程都可以直到这个改动
  • volatile 对错排他的,常常用于多线程的同享变量,在一定条件下,它比锁更适宜,功能开支比锁更少

特色:具有可见性、有序性,不具有原子性

JMM 下 Volatile 的内存语义是怎样的?

  • 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的同享变量值当即改写回到主内存中
  • 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取同享变量。
  • 所以,volatile 的写内存语义是直接改写到主内存中,读的内存语义是直接从主内存中读取的。

4.2 内存屏障

volatile 是经过内存屏障确保它的可见性和有序性的。

那么什么是屏障呢?日子中的比如便是一个栅门,一个红绿灯经过这个操控人流,不允许咱们随意乱窜,从而确保人流车辆的次第。

4.2.1 重排序

重排序是指编译器和处理器为了优化程序功能而对指令序列进行从头排序的一种手法,有时分会改动程序句子的先后次第。

例如:

int i = 0;              
boolean flag = true;
i = 1;                //句子1  
flag = fasle;          //句子2

句子1和句子2的履行次第有或许是先 1后2,也或许是先2 后1 ,这样因为它们数据没有依靠性,重排序后的指令肯定不能改动原有的串行语义。

【注意】:

① 假如存在数据依靠联系,例如句子1界说 i=1;句子2界说 i=2,那么就不能重排序。

② 在多线程下,对存在操控依靠的操作重排序,或许会改动程序履行成果, 这时分需求内存屏障来确保可见性。

【拓宽】

重排序的分类和履行流程

JUC(7) : JMM & Volatile | 死磕内存模型

编译器优化的重排序: 编译器在不改动单线程串行语义的前提下,可以从头调整指令的履行次第

指令级并行的重排序: 处理器运用指令级并行技术来讲多条指令堆叠履行,若不存在数据依靠性,处理器可以改动句子对应机器指令的履行次第

内存体系的重排序: 因为处理器运用缓存和读/写缓冲区,这使得加载和存储操作看上去或许是乱序履行

数据依靠性:若两个操作拜访同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依靠性。

4.2.2 内存屏障

内存屏障,也称内存栅门,是一类同步屏障指令,是 CPU 或编译器在对内存随机拜访的操作中的一个同步点,使得此点之前的一切读写操作都履行后才干够履行此点之后的操作,防止代码重排序。

内存屏障其实便是一种 JVM 指令,Java 内存模型的重排规矩会要求 Java 编译器在生成 JVM 指令时刺进特定的内存屏障指令,经过这些内存屏障指令,Volatile 完结了 Java 内存模型中的可见性和有序性

内存屏障之前的一切写操作都要回到主内存,内存屏障之后的一切读操作都能获得内存屏障之前的一切写操作的最新成果(完结了可见性)

写屏障(Store Memory Barrier):在写指令之后刺进写屏障,强制把写缓冲区的数据刷回到主内存中

读屏障(Load Memory Barrier):在读指令之前刺进读屏障,让作业内存当中的缓存数据失效,从头回到主内存中获取最新数据

4.2.3 分类

JUC(7) : JMM & Volatile | 死磕内存模型

volatile 变量规矩:

JUC(7) : JMM & Volatile | 死磕内存模型

屏障刺进战略

  • 1、在每个 Volatile 写操作的前面刺进一个 StoreStore 屏障
    • 制止前面的普通写和下面的volatile 写重排序
  • 2、在每个 Volatile 写操作的后边刺进一个 StoreLoad 屏障
    • 防止上面的 volatile 写与下面或许有的 volatile 读\写重排序

JUC(7) : JMM & Volatile | 死磕内存模型

  • 3、在每一个 Volatile 读操作后边刺进一个 LoadLoad 屏障
    • 制止下面的一切普通读操作和上面的 volatile 读重排序
  • 4、在每个 volatile 读操作后边刺进一个 LoadStore 屏障
    • 制止下面一切的普通写操作和上面的volatile 读重排序

JUC(7) : JMM & Volatile | 死磕内存模型

4.3 volatile 特性

4.3.1 确保可见性

确保不同线程对某个变量完结操作后成果及时可见,即该同享变量一旦改动了就能告知到其他线程获取主内存的值。

事例:

  • 不加 volatile,没有可见性,程序无法中止
  • 加了 volatiel,确保可见性,程序可以中止。
public class VolatileTest {
    static volatile boolean flag =true;
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println("come in");
            while (flag){
            }
            System.out.println("flag to false");
        }).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag=false;
        System.out.println("main flag ="+flag);
    }
}

4.3.2 不确保原子性

关于 volatile 变量具有可见性,JVM 仅仅确保从主内存加载到线程作业内存的值是最新的,也仅是数据加载时是最新的。可是多线程环境下,”数据计算“ 和数据赋值操作或许屡次呈现,若数据在加载之后,若主内存 volatile 润饰变量产生修正之后,线程作业内存中的操作将会报废去读主内存最新值,操作呈现丢失问题。即各个线程私有内存和主内存公共内存中的变量不同步,从而导致数据不共同。

关于多线程修正主内存同享变量的场景有必要运用加锁同步

JUC(7) : JMM & Volatile | 死磕内存模型

关于 volatile 变量,JVM 仅仅确保从主内存加载到线程作业内存的值是最新的,也仅仅数据加载时是最新的。

假如第二个线程在第一个线程读取旧值和写回新值期间读取 i 的域值,也就形成了线程安全问题。

4.3.3 经过制止指令重排确保有序性

关于 volatile 润饰的变量的读写操作,都会参加内存屏障。

  • 每个 volatile 写操作前面都会加 storeStore 屏障,制止上面的写与它重排序
  • 每个 volatile 写操作后边都会加 StoreLoad 屏障,制止下面的读与它重排序
  • 每个 volatile 读操作后边都会加 LoadLoad 屏障,制止下面的读与它重排序
  • 每个 volatile 读操作后边都会加 LoadStore 屏障,制止下面的写与它重排序

JUC(7) : JMM & Volatile | 死磕内存模型

JUC(7) : JMM & Volatile | 死磕内存模型

4.4 运用场景

1、单一赋值可以,复合运算不可(i++)

volatile boolean flag=false

2、状况标志,判别事务是否完毕

/**
 *
 * 运用:作为一个布尔状况标志,用于指示产生了一个重要的一次性事情,例如完结初始化或使命完毕
 * 理由:状况标志并不依靠于程序内任何其他状况,且通常只要一种状况转化
 * 比如:判别事务是否完毕
 */
public class UseVolatileDemo
{
    private volatile static boolean flag = true;
    public static void main(String[] args)
    {
        new Thread(() -> {
            while(flag) {
                //do something......
            }
        },"t1").start();
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> {
            flag = false;
        },"t2").start();
    }
}

3、开支较低的读、写锁战略

当读远多于写

public class UseVolatileDemo
{
    /**
     * 运用:当读远多于写,结合运用内部锁和 volatile 变量来削减同步的开支
     * 理由:使用volatile确保读取操作的可见性;使用synchronized确保复合操作的原子性
     */
    public class Counter
    {
        private volatile int value;
        public int getValue()
        {
            return value;   //使用volatile确保读取操作的可见性
              }
        public synchronized int increment()
        {
            return value++; //使用synchronized确保复合操作的原子性
               }
    }
}

4、DCL 双端锁的发布

两层查看确定

public class SafeDoubleCheckSingleton
{
    private static SafeDoubleCheckSingleton singleton;
    //私有化构造办法
    private SafeDoubleCheckSingleton(){
    }
    //两层锁规划
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创立目标时,会经过加锁确保只要一个线程能创立目标
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //危险:多线程环境下,因为重排序,该目标或许还未完结初始化就被其他线程读取
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        //2.目标创立完毕,履行getInstance()将不需求获取锁,直接返回创立目标
        return singleton;
    }
}

五、小结

1、因为 CPU 并非直接操作内存,而是将内存的数据放到高速缓存区中,经过高速缓冲区处理了主内存与 CPU 之间的一个速率读取问题。但因为高速缓存区和主存各有一个数据,带来了缓存不共同问题。

2、为了处理缓存不共同问题,Java 推出了 JMM 标准,意图是处理因为多线程经过同享内存通讯时,产生的主内存和作业内存数据不共同性问题、编译器对代码的重排序等问题。

3、那么什么是 JMM 标准呢?它是经过一组规矩来决定一个线程对同享变量的写入何时对另一个线程可见。它有三大特性:可见性、有序性、原子性。

  • 可见性即修正的变量对另一个线程可见,
  • 有序性是因为 JVM 会对机器指令进行重排序,多线程下回呈现乱序现象
  • 原子性是一个线程的操作不能被另一个线程打断

4、那为了处理这三大问题,是不是咱们都要经过加锁或 volatile 等办法处理呢?答案不是的,JMM 给咱们供给了 happens-before 准则来辅佐咱们确保程序的原子性、可见性、有序性问题,它可以判别数据是否存在竞赛、线程是否安全的依据。

5、而 volatile 它可以确保可见性和有序性,它经过内存屏障完结有序性,经过汇编指令后的 lock 前缀,将数据改写回主存完结可见性,但不能确保原子性。

以上便是 JMM 与 Volatile 的相关内容,感谢阅览。