zoukankan      html  css  js  c++  java
  • volatile

    1.内存一致性模型

    CPU的读/写(以及取指令)单元正常情况下甚至都不能直接访问内存——这是物理结构决定的;CPU都没有管脚直接连到内存。相反,CPU和一级缓存(L1 Cache)通讯,而一级缓存才能和内存通讯。大约二十年前,一级缓存可以直接和内存传输数据。如今,更多级别的缓存加入到设计中,一级缓存已经不能直接和内存通讯了,它和二级缓存通讯——而二级缓存才能和内存通讯。或者还可能有三级缓存。

    由于Cache位于CPU内部,意味着对于多个CPU,缓存之对于所在的CPU可见,那么对于每个CPU在处理数据的时候就不免会造成缓存和主存的数据不一致的问题

    1.1 总线锁定

    当某个CPU处理数据时,其在总线上发出一个LOCK#信号,其他处理器就不能操作缓存了该共享变量内存地址的缓存,也就是阻塞了其他CPU,使该处理器可以独享此共享内存。
            但我们只需要对此共享变量的操作是原子就可以了,而总线锁定把CPU和内存的通信给锁住了,使得在锁定期间,其他处理器不能操作其他内存地址的数据,从而开销较大,所以后来的CPU都提供了缓存一致性机制,Intel的奔腾486之后就提供了这种优化。

    1.2 缓存一致性协议(MESI)

    MESI(Modified Exclusive Shared Or Invalid)(也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出)是一种广泛使用的支持写回策略的缓存一致性协议。
    缓存一致性机制就整体来说,是当某块CPU对缓存中的数据进行操作了之后,就通知其他CPU放弃储存在它们内部的缓存,或者从主内存中重新读取。

    • MESI协议中的状态

      CPU中每个缓存行(caceh line)使用4种状态进行标记(使用额外的两位(bit)表示)。

      • M: 被修改(Modified)

      该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。

      • E: 独享的(Exclusive)

      该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。

      • S: 共享的(Shared)

      该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。

      • I: 无效的(Invalid)

      该缓存是无效的(可能有其它CPU修改了该缓存行)。

    • MESI机制说明

      MESI缓存一致性协议说明图

      当CPU A将主存中的x cache line读入缓存中时,此时X副本的状态为E独占。
      当CPU B将主存中的X cache line读入缓存中时,AB同时嗅探总线,得知X cache line不止一个副本,此时X的状态变为S共享
      当CPU A将CACHE A中的x cache line修改为1后,Cache A中的X cache line 的状态变为M修改,并发送消息给CPU B,CPU将X cache line的状态变为I无效
      当CPU A确认所有CPU缓存中的都提交了I无效状态,将修改后的值刷新到主存中,此时主存中的X变为了1,此时Cache A中的x cache line变为E独享
      当CPU B需要用到X,发出读取X指令,于是读取主存中的x,于是重复第二步

    • MESI协议的缺点

      在MESI协议里面如何一个CPU要修改一个处于shared状态的变量非常麻烦的。
      1.本地缓存行将会通过寄存器控制器向远程拥有相同缓存行的寄存器发送一个RFO请求(Request For Owner),告诉其他CPU里面的缓存把缓存里面的值为valid状态,然后待收到各个缓存的(valid ack)已经完成无效状态修改的回应之后,
      2.再把自己的状态改为Exclusive,之后再进行修改。
      3.修改后再改为Modified状态,数据写入缓存行。
      上面这几步大家可以看到第一步的时候,CPU需要在等待所有的valid ack之后才会进行下面的操作。这部分就会让CPU产生一定的阻塞,无法充分利用CPU。这个时候就印出来了存储缓冲区 storeBuffer。

      • 存储缓冲区

        存储缓冲区的作用就是修改一个变量的时候,直接执行修改的操作不直接镇对缓存行,而是针对一个叫制作storeBuffer的位置来操作的。这样CPU在执行修改操作的时候,直接把数据写入到storeBuffer里面,并发出广播告知其他CPU,你们的缓存里面需要变为validate状态,然后去执行其他的操作,等接受到validate ack的时候才会回来把缓冲区里面的值写入到缓存行里面.

        Store Bufferes的风险
        第一、就是处理器会尝试从存储缓存(Store buffer)中读取值,但它还没有进行提交。这个的解决方案称为Store Forwarding,它使得加载的时候,如果存储缓存中存在,则进行返回。
        第二、保存什么时候会完成,这个并没有任何保证。

        MESI存储缓冲区与失效队列架构图

      • 无效队列

        存储缓冲器的大小是有限的。如果存储缓冲区满了,那么依然需要等待validate ack的回复后才可以继续执行运算。但是对方的CPU可能当时正在执行其他的操作。无法及时把缓存里面的值改为validate状态,并发回validate ack操作。这个时候为了继续优化这段CPU空闲的时间。就出现了无效队列!
        无效队列的作用就是CPU接收到validate广播的时候马上返回给对方validate ack响应。等当前的操作执行完再回来真正的把缓存里面的值标识为validate状态。

        • 对于所有的收到的Invalidate请求,Invalidate Acknowlege消息必须立刻发送
        • invalidate并不真正执行,而是被放在一个特殊的队列中,在方便的时候才会去执行。
        • 处理器不会发送任何消息给所处理的缓存条目,直到它处理Invalidate。
      • 存储缓存跟无效队列造成的问题

        存储缓存造成的问题就是在对方CPU还没有给我们回答的时候我们已经执行下一步代码了。
        无效队列造成的问题就是在CPU已经给对方应答的时候自己本身还没有去把这个值validate掉。

        为了解决上述问题,处理器提供内存屏障指令(Memory Barrier):
        写内存屏障(Store Memory Barrier):处理器将存储缓存值写回主存(阻塞方式)。
        读内存屏障(Load Memory Barrier):处理器,处理失效队列(阻塞方式)。

    1.3 指令重排序

    指令重排序分为三种,分别为编译器优化重排序、指令级并行重排序、内存系统重排序。如图所示,后面两种为处理器级别(即为硬件层面)

    • 编译器优化重排序

      编译器在不改变程序执行结果的情况下,为了提升效率,对指令进行乱序的编译。例如在代码中A操作需要获取其他资源而进入等待的状态,而A操作后面的代码跟其没有依赖关系,如果编译器一直等待A操作完成再往下执行的话效率要慢的多,所以可以先编译后面的代码,这样的乱序可以提升不小的编译速度。

    • 指令级并行重排序

      处理器在不影响程序执行结果的情况下,将多条指令重叠在一起执行,同样也是为了提升效率

    • 内存系统重排序

      这个跟之前两个不同的是,其为伪重排序,也就是说只是看起来像在乱序执行而已。对于现代的处理器来说,在CPU和主内存之间都具备一个高速缓存,高速缓存的作用主要为减少CPU和主内存的交互(CPU的处理速度要快的多),在CPU进行读操作时,如果缓存没有的话从主内存取,而对于写操作都是先写在缓存中,最后再一次性写入主内存,原因是减少跟主内存交互时CPU的短暂卡顿,从而提升性能,但是延时写入可能会导致一个问题——数据不一致。

    1.4 内存屏障(Memory Barriers)

    编译器方面使用volatile关键字可以禁止指令重排序,而在硬件方面实现禁止指令重排序的则是内存屏障。其中包括硬件层本来就有的LoadBarriers和StoreBarriers 和JVM封装实现的四种内存屏障。

    • LoadBarriers

      在执行屏障后一个操作前,保证已经刷新了缓存的数据,也就是说使缓存失效,强制从内存刷新数据到缓存中。

    • StoreBarriers

      此屏障之前的写入缓存中的数据同步到内存中,并且保证其他线程可见

    2.Java 内存模型(Java Memory Model)

    Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与 Java 编程时所说的变量不一样,只包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
     
    Java 内存模型中规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存(类比缓存理解),线程的工作内存中保存了该线程使用到主内存中的变量拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递(通信)均需要在主内存来完成,线程、主内存和工作内存的交互关系如下图所示

    Java内存模型结构图

    2.1 Java内存区域(运行时数据区域)和内存模型(JMM)关系

    Java 内存区域和内存模型是不一样的东西,内存区域是指 Jvm 运行时将数据分区域存储,强调对内存空间的划分。而内存模型(Java Memory Model,简称 JMM )是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式。

    2.2. JMM操作与规则

    • 操作

      • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
      • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
      • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
      • load(载入):作用于工作内存的变量,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
      • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
      • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
      • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作。
      • write(写入):作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。
    • 规则

      1. 不允许 read 和 load、store 和write 操作之一单独出现
      2. 不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须同步到主内存中。
      3. 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中。
      4. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量。即对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。
      5. 一个变量在同一时刻只允许一条线程对其进行 lock 操作,lock 和 unlock 必须成对出现
      6. 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值
      7. 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作;也不允许去 unlock 一个被其他线程锁定的变量
      8. 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)

    2.3 重排序

    Java程序的排序与1.内存一致性模型->1.3指令重排序相同

    Java源代码经历的重排序

    2.4 内存屏障

    java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是CPU内存屏障的组合,完成一系列的屏障和数据同步功能。

    • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
    • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
    • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
    • StoreLoad屏障: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

    参考文章

    3.并发编程中的三个概念

    3.1 原子性

    即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

    在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

    Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性.

    3.2可见性

    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

    通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性

    final 关键字的可见性是指:被 final 修饰的字段在声明时或者构造器中,一旦初始化完成,那么在其他线程无须同步就能正确看见 final 字段的值。这是因为一旦初始化完成,final 变量的值立刻回写到主内存。

    3.3有序性

    即程序执行的顺序按照代码的先后顺序执行

    volatile关键字来保证一定的“有序性”

    通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

    Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

    • 3.3.1 happens before

      从JDK 5开始,Java使用新的JSR-133内存模型,JSR-133使用happens-before的概念来阐述操作之间的内存可见性:在JMM中,如果一个操作执行的结果需要对另一个操作可见(两个操作既可以是在一个线程之内,也可以是在不同线程之间),那么这两个操作之间必须要存在happens-before关系:

      • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
      • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
      • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
      • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
        一个happens-before规则对应于一个或多个编译器和处理器重排序规则

    4.volatile特性

    volatile 变量具有两种特性:

    • 保证变量对所有线程的可见性。
    • 禁止进行指令重排序

    4.1 内存屏障

    volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:

    • 在每个volatile写操作的前面插入一个StoreStore屏障;
    • 在每个volatile写操作的后面插入一个StoreLoad屏障;
    • 在每个volatile读操作的后面插入一个LoadLoad屏障;
    • 在每个volatile读操作的后面插入一个LoadStore屏障。
      需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障

    volatile写插入内存屏障示意图

    volatile读插入内存屏障示意图

    4.2 原子性

    volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性

    在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

    4.3 有序性

    在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
    volatile关键字禁止指令重排序有两层意思:
    1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
    2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

    参考文章

  • 相关阅读:
    MySQL JDBC驱动 01 Class.forName
    Sybase性能调试 Statistics
    MySQL InnoDB存储引擎 MySQL介绍
    Sybase性能调试 dbcc trace
    ASP.NET页面的生命周期
    注册JavaScript?
    泛型
    静态类和静态类成员
    构造函数
    MYSQL常用操作
  • 原文地址:https://www.cnblogs.com/donfaquir/p/13751764.html
Copyright © 2011-2022 走看看