• JVM并发机制的探讨——内存模型、内存可见性和指令重排序


    【转】http://my.oschina.net/chihz/blog/58035 文章写的非常好,为作者点赞。

    JAVA内存模型

      对于我们平时开发的业务应用来说,内存应该是访问速度最快的存储设备,对于频繁访问的数据,我们总是习惯把它们放到内存缓存中,有句话不是说么,缓存就像是清凉油,哪里有问题就抹一抹。但是CPU的运算速度比起内存的访问速度还要快几个量级,为了平衡这个差距,于是就专门为CPU引入了高速缓存,频繁使用的数据放到高速缓存当中,CPU在使用这些数据进行运算的时候就不必再去访问内存。但是在多CPU时代却有一个问题,每个CPU都拥有自己的高速缓存,内存又是所有CPU共享的公共资源,于是内存此时就成了一个临界区,如果控制不好各个CPU对内存的并发访问,那么就会产生错误,出现数据不一致的情况。为了避免这种情况,需要采取缓存一致性协议来保证,这类协议有很多,各个硬件平台和操作系统的实现不尽相同。
     
      JVM需要实现跨平台的支持,它需要有一套自己的同步协议来屏蔽掉各种底层硬件和操作系统的不同,因此就引入了【Java内存模型】。对于Java来说开发者并不需要关心任何硬件细节,因此没有多核CPU和高速缓存的概念,多核CPU和高速缓存在JVM中对应的是Java语言内置的线程和每个线程所拥有的独立内存空间,Java内存模型所规范的也就是数据在线程自己的独立内存空间和JVM共享内存之间同步的问题。下面这两张图说明了硬件平台和JVM内存模型的相似和差异之处。
                                      图1.硬件平台
     
                                       图2.Java内存模型
      
      
     
      Java内存模型规定各个线程所共享的变量存在共享内存(main memory)中,每个线程都有自己独立的工作内存,线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中。如何保证多个线程操作主内存的数据完整性是一个难题,Java内存模型也规定了工作内存与主内存之间交互的协议,首先是定义了8种原子操作:

    (1) lock:将主内存中的变量锁定,为一个线程所独占
    (2) unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量
    (3) read:将主内存中的变量值读到工作内存当中
    (4) load:将read读取的值保存到工作内存中的变量副本中。
    (5) use:将值传递给线程的代码执行引擎
    (6) assign:将执行引擎处理返回的值重新赋值给变量副本
    (7) store:将变量副本的值存储到主内存中。
    (8) write:将store存储的值写入到主内存的共享变量当中
    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
      我们可以看到,要保证数据的同步,lock和unlock定义了一个线程访问一次共享内存的界限,有lock操作也必须有unlock操作,另外一些操作也必须要成对出现才可以,像是read和load、store和write需要成对出现,如果单一指令出现,那么就会造成数据不一致的问题。Java内存模型也针对这些操作指定了必须满足的规则:
    (1) read和load、store和write必须要成对出现,不允许单一的操作,否则会造成从主内存读取的值,工作内存不接受或者工作内存发起的写入操作而主内存无法接受的现象。
    (2) 在线程中使用了assign操作改变了变量副本,那么就必须把这个副本通过store-write同步回主内存中。如果线程中没有发生assign操作,那么也不允许使用store-write同步到主内存。
    (3) 在对一个变量实行use和store操作之前,必须实行过load和assign操作。
    (4) 变量在同一时刻只允许一个线程对其进行lock,有多少次lock操作,就必须有多少次unlock操作。在lock操作之后会清空此变量在工作内存中原先的副本,需要再次从主内存read-load新的值。在执行unlock操作前,需要把改变的副本同步回主存。
    ===============================================================================================

    内存可见性

    通过上面Java内存模型的概述,我们会注意到这么一个问题,每个线程在获取锁之后会在自己的工作内存来操作共享变量,在工作内存中的副本回写到主内存,并且其它线程从主内存将变量同步回自己的工作内存之前,共享变量的改变对其它线程是不可见的。那么很多时候我们需要一个线程对共享变量的改动,其它线程也需要立即得知这个改动该怎么办呢?比如以下的情景,有一个全局的状态变量open:
    boolean open= true;
    这个变量用来描述对一个资源的打开关闭状态,true表示打开,false表示关闭,假设有一个线程A,在执行一些操作后将open修改为false:
    //线程A
    resource.close();
    open = false;
    线程B随时关注open的状态,当open为true的时候通过访问资源来进行一些操作:
    //线程B
    while(open) {
            doSomethingWithResource(resource); 
    }
    当A把资源关闭的时候,open变量对线程B不可见,如果此时open变量的改动尚未同步到线程B的工作内存中,那么线程B就会用一个已经关闭了的资源去做一些操作,因此产生错误。

    所以对于上面的情景,要求一个线程对open的改变,其他的线程能够立即可见,Java为此提供了volatile关键字,在声明open变量的时候加入volatile关键字就可以保证open的内存可见性,即open的改变对所有的线程都是立即可见的。volatile保证可见性的原理是在每次访问变量时都会进行一次刷新,因此每次访问都是主内存中最新的版本。
    ======================================================================================================================
    重排序
    |---编译期重排序
        编译期重排序的典型就是通过调整指令顺序,在不改变程序语义的前提下,尽可能减少寄存器的读取、存储次数,充分复用寄存器的存储值。
    重排序有利于充分利用流水线,使指令能够高效并行执行。
    |---运行期重排序
        
    Java内存模型
    在Java存储模型(Java Memory Model, JMM)中,重排序是十分重要的一节,特别是在并发编程中。JMM通过happens-before法则保证顺序执行语义,如果想要让执行操作B的线程观察到执行操作A的线程的结果,那么A和B就必须满足happens-before原则,否则,JVM可以对它们进行任意排序以提高程序性能。
    volatile关键字是可以保证变量的可见性的,但是不能保证其原子性。因为对volatile的操作都是在main memory中的,main memory使所有线程所共享的,这样就会使性能降低。
    volatile修饰的变量,能够阻止重排序的发生,
     

    JVM并发机制的探讨——内存模型、内存可见性和指令重排序

  • 相关阅读:
    KMP字符串匹配算法
    彩色图像处理之色彩学基础
    Dijkstra单源最短路径算法
    论文笔记 [6] 图像幻构中的 Feature Enhancement
    论文笔记 [5] SRCNN
    论文笔记 [4] ARCNN(Artifacts Reduction CNN)
    论文笔记 [3] CNN去compression artifacts
    LeetCode小白菜笔记[11]:Search Insert Position
    论文笔记[2] 基于深度学习的CNN图像质量评估和预测
    Geophysics背景知识(3)
  • 原文地址:https://www.cnblogs.com/plxx/p/5129714.html
Copyright © 2020-2023  润新知