lsp都要会的内存模型

兄弟们好,给你们带来一篇内存模型的水文(手动滑稽)。Beginjava

先来看大纲面试

1.JMM规范

先来讲JMM是什么?数据库

JMM(Java Memory Model):全称Java内存模型。它定义了Java虚拟机在计算机内存中的工做方式。它是一套规范,并不真实存在。它包括三个点:原子性,可见性,有序性缓存

首先咱们来看一下它的工做原理。线程操做数据的时候须要从主内存中读取,线程操做完数据之后进行写回主内存。安全

可能有的兄弟要说了,为何要搞这么麻烦呢?我直接操做主内存中的数据不就得了,干吗非要复制一份再用。多线程

咱们经过一个场景来讲明这个问题。架构

假设如今不存在JMM规范,咱们全部的操做都是直接在主内存中完成。并发

单线程:咱们定义了一个资源,而后在这个线程中使用这个资源,对于这个资源的修改等操做直接在主内存中完成。没有任何问题。app

多线程:仍是同样,咱们定义了一个资源,而后再多个线程中使用了这个资源,不一样线程中对资源的修改所有直接操做主内存,这个时候不一样线程之间的操做可能被相互覆盖。优化

也就是在并发操做下会出现资源覆盖的状况,因而引入了Java内存模型的概念

聊完了为何要用这个东东,咱们再来聊一下它的三个特色:

1.1 原子性

原子这个名词最开始接触应该是咱们在高中时期的化学吧。当时最直接的解释就是元素最小的构成单位。固然最后又出现了夸克这样的东东。

在其余领域中逐渐把原子做为了一个最小的单位,例如计算机,一个原子操做表明了这个操做不能够被打断即最小不可被分割。

原子性的含义就是这个:它表示这个操做不能够被中断

再聊原子性,在计算机领域中接触到的应该就是数据库的事务了吧。四大特性:原子性,一致性,隔离性,持久性。其中的原子性含义和上述相同,表明了这个事务中的操做不能够被中断,要么成功要么失败。

1.2 可见性

不知道兄弟们看不看小说,好多玄幻小说的主角,拿到一本远古秘籍就本身修炼,不给其余人看(想不通,为何不给其余人看),此时这本秘籍相对于其余人来讲就是不可见的。而在现代图书馆,全部的书你都能看到,这个时候这些书籍就是可见的。

在计算机领域中,可见性的表现和上述的故事相似,只不过表现形式为线程之间的数据是否可见。好比:线程A和线程B同时从主内存中读取了一个资源,而后分别作了修改,不过线程A的操做更快一些,在线程B写回以前就将该资源写回了主内存(JMM规范),可是这个时候线程B不知道作了修改,此时它进行了写回,这个时候咱们称这个资源对于不一样线程是不可见的。一句话说明白:不一样线程之间不能共享资源状态的称为该资源不具备可见性

那若是这个资源存在可见性,那么当线程A将资源从新写回主内存的时候,就会触发一个机制,使其余线程从新从工做内存中读取最新的资源,而后进行操做,这个时候就表明该资源具有可见性。一句话说明白:不一样线程之间能够共享资源状态称为该资源具备可见性

画个图玩一下。

1.3 有序性

这个很好理解,咱们直接上解释。开发者编写代码的时候都是按照必定的顺序进行编写,而在具体执行的时候不必定会按照咱们本身写的顺序执行,JVM会进行必定的优化,对代码进行重排,提升代码的执行速率。

重排序类型:

  • 编译器优化重排序。在不改变单线程执行语义的状况下,编译器从新梳理代码的执行顺序
  • 指令级并行重排序。现代处理器采用了指令级并行技术,将多条指令重叠执行,若是不存在数据依赖性,处理器能够适当改变语句对应机器指令的执行顺序。
  • 内存系统重排序,因为处理器使用缓存和读/写缓存区,这使得加载和存储操做看上去是乱序执行。

搞明白这些执行重排序的东东之后,咱们再看一下重排序出现的时机。

单线程执行语义:as if serial 表示在单线程状态执行的状况下,重排序之后的代码和没有进行重排序的代码执行结果相同,即重排序不会影响代码的正确性。

数据依赖性:表示两条指令之间不存在数据依赖,主要表现形式为如下的几种状况

只要保证了单线程执行语义和不出现上图所述的几种数据依赖关系就会出现指令重排序。

2. JMM内存交互

在经过JMM规范进行内存交互,依赖于下面八大内存交互操做

  • lock做用于主内存变量,把一个变量标识为线程独占。其余线程不能进行操做
  • unlock做用于主内存变量,把一个变量从线程独占状态,变为公有状态
  • read做用于主内存变量,将主内存中的变量传输到工做内存中
  • load做用于工做内存变量,将read传输到工做内存中的变量加载到工做内存中
  • store做用于主内存变量,将工做内存中的变量加载到主内存中
  • wirte做用于工做内存变量,将store操做中的变量写入主内存中
  • use做用于工做内存变量,当使用这个变量的时候,会经过这个指令来完成
  • assign做用于工做内存变量,当为这个变量进行赋值的时候,会经过这个指令完成

JMM规范定义了如下的指令出现操做

  • readload操做必须同时出现;storewrite必须同时出现
  • 不容许线程丢弃其assign操做,若是线程对资源进行修改则必须通知主内存
  • 不容许线程将一个没有进行assign的资源直接同步到主内存中
  • 不容许线程直接建立一个资源,全部的资源必须经过主内存常见,读取,才能操做。也就是说在进行assignstore以前必须先进行loaduse。(待会详细解释
  • 一个变量在同一个时刻只能被一个线程执行一次lock操做;但能够被同一个线程执行屡次(可重入锁),可是lock的次数和unlock的次数应该保证相同
  • 对一个变量执行了lock操做,在unlock的时候全部的线程必须从新读取主内存中该变量的值。(synchronized的可见性实现原理)
  • 若是一个变量没有被lock则不能对其进行unlock操做
  • 对一个变量执行unlock操做的时候,必须将该变量同步到主内存中。

详细和兄弟们聊聊第四条,不知道兄弟们还记不记得一个对象被存放的位置

看完上图,是否是对上面JMM定义的内存交互规则里的第四条,有了些许疑问。咱们再来看一遍第四条

不容许线程直接建立一个资源,全部的资源必须经过主内存常见,读取,才能操做。也就是说在进行assignstore以前必须先进行loaduse

按照上面说的含义,全部的资源都是在主内存中建立,工做内存只能经过readload,读取加载之后才能use,而咱们上面的一个对象的建立过程彷佛违背了这个原则。(能够认为,工做内存对应了栈,主内存对应了堆)

这个操做实际上是因为JVM对常见对象的过程作的一个优化,节省因为共享内存而形成的一系列开销。

有兄弟要说了,你给我讲这些,我在实际中怎么进行分析呢???

没错,这玩意在实际中的确蛮难分析的,因此呢咱们的大JAVA提供了一个叫Happen Before的原则用来分析操做的安全性

2.1 Happen-Before

全称:先行发生原则。

大白话:操做A先于操做B发生,则在执行操做B的时候操做A的全部修改,均可以被操做A观察到。

3. Synchronized实现JMM规范

3.1 原子性

兄弟们应该还记得上面咱们在介绍JMM规范的时候对于原子性的相关解释。

synchronized代码块能够保证在该代码块中仅仅存在一个线程正在执行,因此保证了这个代码块中的原子性,不能被其余线程所中断

3.2 可见性

兄弟们还记不记得咱们在上面聊内存交互的时候提到了八个指令其中存在一个lockunlock,并且在JMM对这八个指令定义规则时候其中有一条:unlock之后其余工做内存须要从新从主内存读取这个变量的最新值

synchronized的底层实现就是lockunlock原子指令(能够看JVM源码)。

synchronized代码块执行完成之后,会触发工做内存变量的刷新机制,保证变量的可见性。

画张图看看

3.3 有序性

咱们来看一段代码

private int num = 0 ;
    private boolean flag = false ;

    public int test01(){
        synchronized (this){
            if(!flag){
                num = 2 ;
            }
        }
        return num ;
    }

    public void test02(){
        synchronized (this){
            num = 4 ;
            flag = true ;
        }
    }

能够发现synchronized没有从根源禁止指令重排序,实际上指令重排序仍是发生了,只不过因为加锁了,致使其余线程没法进入加锁的代码块,因此即便发生了指令重排序也不会对程序形成任何影响。

4. Volatile实现JMM规范

兄弟们应该都听过这样的面试题,聊聊你对Volatile的理解

而咱们经常使用的回答,Volatile保证可见性,有序性,可是不保证原子性

下面咱们来聊聊它是如何保证可见性和有序性。

Volatile实现可见性和有序性都是基于内存屏障实现的。下面咱们仔细聊一下内存屏障是什么

4.1 内存屏障

咱们来看一组代码

x = x + y ;
z = 3 ;

这两行代码的执行顺序不是咱们开发者能够控制的,计算机内部会进行编译优化或者运行优化,也就是说,第二行的代码可能优于第一行代码执行。而咱们想要保证代码的执行顺序,每每须要采起一系列措施,如硬件措施或者软件措施等。

而内存屏障就是在硬件之上,操做系统和JVM之下的对并发作的最后的一层封装。

咱们先来聊一下CPU层面的并发处理方案。

兄弟们应该还记得咱们以前将Synchronized的时候涉及到的CPU的架构

上图就是CPU的架构图,一个CPU两个核心,单独的一级和二级缓存,三级缓存公用。而在并发操做的时候就会出现数据冲突的问题,也就是缓存一致性的问题。而解决这种问题,CPU厂商提供了一个解决方法:MESI协议。

MESI表明了四种状态,下面是对这四种状态的解释

  • M(修改,Modified):本地处理器已经修改缓存行,便是脏行,它的内容与内存中的内容不同,而且此cache只有本地一个拷贝
  • E(专有,Exclusive):缓存行内容和内存中的同样,并且其余处理器都没有这行数据。
  • S(共享,Shared):缓存行规内容和内存中的同样,有可能其余处理器也存在该缓存行的拷贝
  • I(无效,Invalid):缓存行失效,不能使用。

咱们简单的聊一下这个协议在实际中的应用

  • Core1修改值之后,会发送一个信号给其余正在对该值进行操做的核,改变其余核中值得状态
  • Core1修改多少次值,就会发送多少次信号给其余正在进行操做的核。(发信号的时候会锁总线
  • 其余核在使用该值的时候发现该值已经失效了,会从内存中从新读取该值

MESI协议保证了在CPU中缓存的可见性。但在内存中却没法保证其可见性等。因此这个时候就须要内存屏障来解决这个事情了。

其实咱们能够本身思考一下,内存屏障如何作:无非就是在须要保证JMM规范的语句中加入一个块,让CPU或者编译器不对该块内容进行重排序,因此呢,组成这个块的就是两个指令

内存屏障的两大指令

  • load将内存中的数据拷贝处处理器中
  • store将处理器中缓存的数据刷回内存中

咱们看一个工做图

这两个指令组合起来就造成了四种屏障类型

屏障类型 指令说明 说明
LoadLoadBarriers Load1;
LoadLoad;
Load2
确保Load1数据的装载优先于Load2
StoreStoreBarriers Store1;
StoreStore
store2
确保Store1刷新数据到内存(此时数据对其余处理器可见)的操做先于store2的刷新数据到内存中
LoadStoreBarriers Load1;
LoadStore;
Store2
确保Load1的数据状态先于Store2的数据刷回内存种
StoreLoadBarries Store1;
StoreLoad;
Load2
确保store1的数据刷回内存的操做先于Load2的数据装载

其中的StoreLoad Barriers屏障同时具有其余三个屏障的效果,所以被成为全能屏障,可是其开销比较昂贵

4.2 可见性和有序性

Volatile是如何保证可见性和有序性的?

咱们以X86系统架构来讲明,

对于X86系统而言,它仅仅实现了三种内存屏障

Store Barrier

sfence指令实现了Store barrier,至关于咱们上面提到的StoreStore Barriers。它强制把sfence指令以前的store指令所有在其以前执行。即禁止sfence先后的store指令跨越sfence执行,而且全部在sfence以前的内存更新都是可见的

Load Barrier

lfence指令实现Load Barrier,至关于咱们前面提到的loadLoad Barriers它强制全部在lfence指令以后的load所有在lfence以后执行。配合StoreBarrier使用,使得sfence以前的内存更新对与lfencec以后的Load操做都是可见的

Full Barrier

mfence指令实现了Full Barrier至关于StoreLoad Barriers

它强制全部的mfencec指令以前的store/load指令都在该指令执行以前执行,保证了mfence先后的可见性和有序性

JVMVolatile变量的处理。

  • 在写volatile变量以后插入一个sfence,保证sfence以前的写操做不会被重排序到sfence以后,同时保证其变量的可见性。
  • 在读volatile变量以前,插入一个lfence,这样保证了lfence以后的读操做不会跑到lfence以前。

5. Final实现JMM规范

一个字段被声明为finalJVM会在初始化final变量后插入一个sfence,而类的final字段在clinit方法中初始化,由类加载过程保证其可见性,而你内存屏障保证了重排序,因此其实现了可见性和从有序性。

相关文章
相关标签/搜索