JVM---Java内存模型

前言 

  计算机并发执行多个任务 与 充分利用计算机CPU性能 的关系 没有想象的简单,运算任务不可能只靠CPU就完成,CPU至少需要 与 内存进行交互(读写数据),IO操作无法消除;

  但CPU的运算速度 与 存储设备的 速度 有几个数量级的差距;

  所以又引入 接近CPU运算速度的高速缓存Cache,让运算快速进行,当运算结束后 从Cache同步到内存;

  但 引入Cache后,又引入 缓存一致性问题;

  在多CPU的系统中,多个CPU都有自己的Cache 又 共享同一个内存,将会导致各自的缓存数据不一致;如果发生缓存数据不一致,以哪个CPU的Cache为准?

  为了解决缓存一致性问题,各CPU访问Cache时,要遵循一定的协议(MSI、MESI...),读写按照协议进行;

  

  “内存模型” 可以理解为 在特定操作协议下,对特定内存或缓存 进行读写访问的 抽象过程;

  不同架构的物理机器可以有不同的内存模型;

  

  JVM也有自己的内存模型;

  除了增加Cache外,为了使CPU的运算得到充分利用,CPU会对 输入的指令 进行 乱序执行优化 ,CPU会将乱序执行后的结果重新组装,保证结果和顺序执行一致;

Java内存模型(Java Memory Model,JMM) 

概述 

  JVM规范视图 定义 一种JMM 屏蔽各种硬件和OS的 内存访问差异,以实现Java程序在任何平台下都能达到一致的内存访问效果;

  C/C++ 直接使用物理硬件和OS的内存模型,由于平台上内存模型不同,所以在不同平台需要不同的程序实现;

  jdk1.5后,JMM已经逐渐成熟和完善;

主内存&工作内存 

  JMM的主要目标:定义 程序中各个变量的 访问规则(在JVM中,将变量存储到内存 、从内存读取变量的底层细节);

    (此处的变量,指实例字段、static字段...,不包括局部变量、方法参数 [线程私有]) 

  为了获得更好的执行效能,JMM没有限制 执行引擎使用特定的寄存器或缓存 与 内存交互,也没限制JIT调整代码执行顺序进行优化;

  

  JMM规定 所有的变量都存储在主内存中;

  每个线程有自己的工作内存,

    线程的工作内存 保存了被该线程使用的变量的主内存副本

    线程对 变量的读写操作 必须 在工作内存中进行(不能直接读写主内存的变量); 

    不同线程之间 不能直接访问 对方工作内存的变量;

    不同线程之间 变量值的传递 只能通过 主内存来完成;

    

主内存&工作内存 交互协议

  交互协议:一个变量 如何从主内存 拷贝到  工作内存、如何从工作内存 同步 到主内存;

  JMM定义了8种操作来完成,每种操作都是原子操作

    Lock:

      作用于 主内存变量;

      把一个变量 标识为 一条线程独占状态;

    unLock:

      作用于 主内存变量;

      把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程Lock;

    read(读取):

      作用于 工作内存变量; 

      把 主内存的变量值 传输到 线程的工作内存,以便后面的load使用;

    load(载入):

      作用于 工作内存变量;

      把Read操作 从主内存获取的变量值 存储到 线程工作内存的变量中;

    use(使用):

      作用于 工作内存变量;

      把 工作内存变量值 传递给 执行引擎;

    assign(赋值):

      作用于 工作内存变量;

      把 从执行引擎 接收到的值 赋值给 线程工作内存变量;

    store(存储):

      作用于 工作内存变量;

      把 工作内存变量的值 传送到 主内存中,以便后面的write使用;

    write(写入):

      作用于 主内存变量;

      把 store操作 从 工作内存获得的变量值 写入到 主内存变量中;

  

  把一个变量 从主内存 copy到 工作内存,需要顺序执行Read、load操作;

  把一个变量 从工作内存 同步到 主内存,需要顺序执行store、write操作;

    (注意:JMM只要求上述2对必须顺序执行,没有保证连续执行)

  

  除此之外,JMM还规定在执行上述8个操作时必须满足以下规则

    ...

  

  这8种操作+上述规则+volatile特殊规定,已经完全确定Java程序哪些内存访问操作在并发下是安全的;

  由于这种定义相当严谨但又繁琐,实践起来很麻烦,后面会介绍一个等效判断原则---先行发生原则,用来确定一个操作在并发下是否安全;

 

volatile变量的特殊规则

    volatile可以说是JVM提供的 最轻量级的 同步机制

    JMM对volatile变量定义了特殊访问规则:

      当一个变量定义为volatile后,将具备2种特性:

        1、保证此volatile变量 对所有线程的 可见性(当任一线程对该变量修改后,新值对其他线程是立即得知的);

          (volatile变量在各个线程的工作内存不存在一致性问题 [volatile变量在各个线程的工作内存中可以存在不一致,但 每次使用前都要先刷新,执行引擎看不到不一致情况])

          (但 Java程序的运算 并非原子操作,导致 volatile变量在并发下一样是不安全的)

        eg:

static int oo =0;

oo++;


javap -v **.class


0: getstatic     #2    
3: iconst_1
4: iadd
5: putstatic     #2 

          Java程序的volatile i , i++ ,用javap反编译后,由多条指令完成的,虽然get时保证了 i 值的正确性,但 iconst、innc指令由于并发执行,

          其他线程已经对i值进行修改;

        

        由于volatile只能保证多线程的可见性,在不符合以下规则的远算场景,还需要加锁(synchronized或原子类工具)保证原子性:

          运算结果 不依赖 变量的当前值;

          该变量没有包含在具有其他变量的不变式中;

                    

        2、禁止指令重排序优化

          硬件架构上讲,指令重排序指CPU采用了允许将 多条指令 不按程序规定的顺序 分开远算处理;

            (CPU需要能正确 处理指令依赖情况,保证 程序得出正确的计算结果 [指令间有依赖关系的无法重排序])           

          volatile修饰变量后,赋值指令后增加一条内存屏障指令(指令重排序时,不能把后面的指令重排序到内存屏障之前)

volatile变量性能  

    volatile变量 真的比 使用其他同步工具 更快么?

      某些情况下,volatile的同步机制 性能优于 锁;

      但JVM对锁实行的许多消除和优化,使得很难量化地认为volatile就比锁性能好;

      

      如果volatile的读和写对比,读操作和普通变量几乎没什么差别,但是写操作会慢很多(需要在本地代码中插入许多内存屏障指令保证CPU不发生乱序执行);

JMM特征 

  JMM是围绕着 在并发过程中 如何处理 原子性、可见性、有序性 3个特征来建立的: 

    原子性

      由JMM保证 原子性的变量操作 包括:Read、load、assign、use、store、write,大致认为对 基本数据类型 的读写具备原子性(long、Double除外);

      如果应用场景需要更大范围的原子性保证,JMM还提供了Lock、unlock操作;

      (JVM并没有将Lock、unlock开放给用户使用,但提供了更高层次的字节码指令monitorenter、monitorexit隐式使用,反映到Java程序中就是synchronized)        

    可见性

      JMM通过 工作内存中修改变量值 后 同步到 主内存中,在读取变量值 从主内存中获取;

      Java实现可见性:

        volatile:

          变量 特殊规则 保证新值能立即同步到主内存中,且 每次使用工作内存变量值 都从 主内存中重新获取;

        synchronized:

          在对一个变量执行unlock前,必须先把 工作内存的变量值 同步到 主内存中;

        final:         

          被final修饰的变量 在构造器中一旦初始化完成, 且 构造器没有把this传递出去,其他线程就能看到final的值;

            (this引用逃逸是件很可怕的事情,其他线程有可能使用这个引用访问到初始化一半的数据)          

    有序性 

      如果在本线程内观察,所有的操作都是有序的(线程内表现为串行);

      如果在一个线程中观察另一个线程,所有的操作都是无序的(指令重排序现象 & 工作内存与主内存 同步延迟现象);    

      Java提供了 synchronized、volatile保证线程之间操作的有序性:

        volatile:禁止指令重排序

        synchronized:主内存的变量 在同一时刻 仅允许一条线程 对其进行Lock操作;        

先行发生原则(happens-before)  

  如果JMM中所有的 有序性 都要靠volatile和synchronized来实现,那么操作将会变得很繁琐;

  但是在编写Java并发代码时,并没有感觉到这点,因为Java语言有个先行发生(happens-before)原则;

  这个原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据;

  

  先行发生原则:

    JMM中定义的两项操作之间的偏序关系;

    (如果操作A 先行发生于 操作B,其实就是说 在发生操作B之前,操作A产生的影响 [修改共享变量值、发送消息、调用方法...] 能被操作B观察到) 

    

  JMM下 先行发生 关系

    (这些先行发生关系 无需 任何同步器协助 就已经存在,可以在编码中直接使用;

      如果2个操作之间的关系不在此列,且 无法从下列规则 推导出来,就没有顺序性保证,JVM可以对它们随意重排序)

    程序次序规则

      同一个线程内,按照程序代码的顺序,书写在 前面的操作 先行于 后面的操作(严格说是 控制流顺序);

    管程锁定规则

      对于同一个锁,一个线程的unlock操作 先行发生于 后面另一个线程对同一个锁的Lock操作;

    volatile变量规则

      一个线程对volatile变量 的写操作 先行于 后面另一个线程对这个volatile变量的读操作;

    线程启动规则

      Thread对象的start方法 先行于 此线程的每个操作;

    线程终止规则

      线程中的所有操作都 先行于 对此线程的终止检测;

    线程中断规则

      对线程interrupted方法的调用 先行于 被中断线程的代码检测到中断事件的发生;

    对象终结规则

      一个对象的初始化完成 先行于 finalize方法的开始;

    传递性

      如果操作A 先行于 操作B,操作B 先行于 操作C,可以得出 操作A 先行于 操作C;

 

  时间先后顺序 与 先行发生原则 没有太大关系,衡量并发安全问题时 不要受到时间先后顺序的干扰,必须 以先行发生原则 为准

    

posted on 2019-12-11 19:51  anpeiyong  阅读(338)  评论(0编辑  收藏  举报

导航