JAVA之JUC系列 - JAVA内存模型

    Java内存模型(简称JMM),定义了线程本地内存和主内存之间的关系,理解JMM的特性,对深入理解Java多线程中内存的可见性会有很大帮助。下面我们从并发编程模型中关注的两个问题说起。

一. 并发编程模型中关注的两个问题

    在一般的并发编程中,通常会关注两个方面的问题,一个是线程之间的通信,一个是线程之间如何同步。

    (1)通信是指线程之间已何种方式来交换信息,在常用的编程模型中,线程之间的通信方式有两种:共享内存和消息传递

             在共享内存的并发模型中,线程之间共享程序的公共变量,通过读写内存中的公共变量达到隐式的通信;在消息传递的并发模型中,线程之间通过发送消息来达到显示的通信。

    (2) 同步是指程序中控制不同线程的操作发生相对顺序的一种机制。

            在共享内存的并发模型中,同步是显式的,程序员必须在代码中显式的指定某个方法或者某个代码块是需要线程同步的;在消息传递的并发模型中,发送消息的事件必须发生在接受消息之前,线程之间的同步是隐式的。

    Java采用的是共享内存的并发模型,线程之间通过共享变量进行隐式的通信,整个通信的过程对程序员完全透明。线程之间的同步是显式的,程序员必须在代码中指定何时需要线程同步。

二. Java内存模型抽象

        在JAVA中,有两种类型的变量:共享变量和局部变量,共享变量包括实例变量,静态变量,数组元素。这些元素都存在JAVA堆中,堆中的内存是线程共享的。局部变量包括方法参数,方法中定义的变量,异常处理参数这些变量不会在线程中共享,所以也不会存在内存可见性问题,JMM模型也不会对其产生影响。

        JAVA中线程的通信由JMM控制,JMM决定一个线程写入的共享变量什么时候对另外一个线程可见。从抽象上JMM定义了  线程本地内存与主内存之间的关系:线程之间通过主内存中共享变量来进行通信,每个线程拥有自己的本地内存,本地内存存储了线程读写共享变量的副本,本地内存是一个抽象的概念,可能包括:缓存,写缓存区,CPU寄存器以及其他的硬件和编译器优化。JAVA抽象的内存模型如下图:

《JAVA之JUC系列 - JAVA内存模型》

从上图看,线程一和线程二通信,需要两个步骤:

   (1)线程一将本地内存A中的共享变量刷新到主内存。

   (2)线程二从主内存中取出线程一刷新过后的共享变量

《JAVA之JUC系列 - JAVA内存模型》

    本地内存A和本地内存B通过主内存共享变量x的副本。假设初始状态这三个内存中的x值都是0,当线程一运行时,将x值更新为1,并保存到自己的本地内存中,当线程一与线程二需要通信时,线程一需要把本地内存A中的x值刷新到主内存,然后B再从本地内存中读取线程一刷新后的x值。

    由此可见,线程一与线程二进行通信,必须要经过主内存,JMM通过控制本地内存与主内存的交互,来保证内存的可见性。

三. 重排序

    执行程序时,为了提高效率,编译器或处理器会对指令进行重排序,有3种类型的重排序:

       (1) 编译器优化重排序

       (2) 指令级并行重排序

       (3) 内存系统重排序

     JAVA源代码到最终执行的指令序列,会分别经历上面3种重排序,其中(1)属于编译器重排序,(2)(3)属于处理器重排序

    这些重排序可能会导致多线程程序的内存可见性问题,对于编译器重排序,JMM的编译器重排序规则会禁止特定类型的编译器重排序;对于处理器重排序JMM重排序规则会要求Java编译器在生成指令序列时,插入特定的内存屏障(Memory Barrier),来禁止特定类型的处理器重排序。JMM确保在不同的编译器和处理器上通过禁止特定类型的编译器重排序和处理器重排序,来为程序员提供一致的内存可见性保证。

   

四. 内存屏障(Memory Barrier)介绍

    现代处理器使用写缓冲区临时保存向内存写入的数据。写缓冲器会保证指令流水线持续的运行。它避免了CPU等待想内存写入数据而产生的延迟。虽然写缓冲器带来了好处,但每个缓冲器数据仅对它所在的CPU可见,这个特性造成了CPU对内存的读写顺序未必与内存实际发生的读写操作顺序相一致。

    处理器A执行  : a=1;x=b;  处理器B执行: b=1;y=a;   程序执行的最终结果可能是 : x=y=0

    当处理器A和处理器B同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另外一个共享变量(A2,B2),最后才把自己写缓冲区中的数据刷新到内存中(A3,B3)。当CPU以这种时序执行的时候,程序最终的到的就是x=y=0这种结果。虽然处理器A执行内存操作的顺序为:A1->A2,但内存操作实际发生的顺序确实A2->A1。此时处理器A的内存操作顺序被重排序了。

《JAVA之JUC系列 - JAVA内存模型》

   由于现代处理器都会使用写缓冲器,因此现代处理器都会允许写-读操作的重排序。

    因此为了保证内存可见性,Java编译器会在生成指令序列时在适当位置插入内存屏障来禁止特定类型的处理器重排序,JAVA把内存屏障指令分为4类:

    《JAVA之JUC系列 - JAVA内存模型》

          StoreLoad屏障是一个全能型的内存屏障,他同时支持以上3种屏障的效果,目前大部分CPU都支持这种内存屏障,不过StoreLoadBarrier的开销很大,CPU会将屏障前所有指令写入缓存的数据刷进主内存。

五. happens-before 介绍

         从JDK1.5开始,JAVA使用JSR-133内存模型,JSR-133内存模型使用happens-before规则来阐述内存的可见性问题,一个操作的执行结果要对另外一个操作可见,这两个操作之间必须存在happens-before关系,这两个操作可以在一个线程中,也可以发生在两个线程中。

        JSR-133规定的happens-before规则如下:

  •         程序顺序规则:在同一线程中,一个操作happens-before任意该操作的后续操作
  •         监视器锁规则:一个锁的解锁happens-before 随后对这个锁的加锁操作。
  •         volatile规则:对一个volatile变量的写操作,happens-before后续对这个变量读操作。
  •         传递性规则:如果A happens-before B,B happens-before C,则A happens-before C 

  happens-before规则与JMM关系如下:

《JAVA之JUC系列 - JAVA内存模型》

由此可见一个happens-before规则对应一个或多个编译器或处理器重排序规则,它在逻辑上为程序员处理内存可见性问题提供了同一的简单视图。       

    原文作者:JUC
    原文地址: https://blog.csdn.net/brightliming/article/details/80761619
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞