java基础—java内存模型(JMM)CPU架构缓存一致性重排序JMM的实现JMM保证可见性有序性问题的详解

Posted zero

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java基础—java内存模型(JMM)CPU架构缓存一致性重排序JMM的实现JMM保证可见性有序性问题的详解相关的知识,希望对你有一定的参考价值。

java基础—JMM(CPU架构、JMM保证可见性、有序性)

文章目录

CPU架构

说一说现在计算机存储器的结构层次

分为CPU内部和外部

  • CPU内部:有CPUL0、L1、L2缓存
  • CP外部:CPU外部就是主存、磁盘。
  • CPU缓存特别快,比内存快上100个数量级,比硬盘快一百万个数量级。最慢的是远程存储。
  • 高速缓存作为内存与处理器之间的缓冲:将运算需要的数据从主存复制到CPU高速缓存中,让运算快速进行,当运算结束后再从缓存中刷回主存。
  • 使用CPU缓存的作用就是提高运算效率
  • 采用这种高速缓存会存在问题:由于每个cpu都有自己的一个高速缓存,全部共享一个主存。当多核CPU同时访问同一块内存区域时,可能导致各自的缓存不一致,此时就需要用到缓存一致性协议。

使用CPU缓存,会导致什么问题?

会出现缓存一致性问题。由于各个CPU有了自己的缓存,从主存中读取数据到自己的缓存中更新数据,再刷回主存。在多线程情况下会产生缓存一致性问题。

int a=5;
public int sum()
return a+=5;

例如线程1和线程2执行sum方法,操作都是将主存中的a=5分别+5,线程1读取后更新为a=10,还没刷回主存,此时线程2读取到主存中a=5,将a更新为10,之后全部更新到主存。此时a=10。


解决缓存一致性有哪些方案

  1. 总线锁定: 处理器提供lock#信号,当其中一个核心在总线上输出lock#时,其它处理器的请求将会被阻塞,该处理器独占内存。总线锁这种做法锁定的范围太大了,导致CPU利用率急剧下降,因为使用LOCK#是把CPU和内存之间的通信锁住了,这使得锁定时期间,其它处理器不能操作其内存地址的数据 ,所以总线锁的开销比较大
  2. 缓存锁+总线锁: 现代CPU使用的是缓存一致性协议+总线锁一起保证了缓存的一致性。当其它核心更新了数据写回被核心1锁定的缓存行时,缓存将会失效。但是并不是所有情况缓存锁定都会有效,有两种情况会降级为总线锁(1)数据跨多个缓存行的情况,缓存锁定将会失败,转而降级为总线锁定。(2)老的CPU不支持缓存锁定。
  3. 总线嗅锁: CPU的写事务会被窥探器在总线上嗅探到,窥探器会检查该变量的副本是否在其他核心缓存上也有一份,如果有该副本,则窥探器会执行策略来保证副本的一致性,这个策略可以使刷新缓存或者让缓存失效,这取决于缓存一致性协议的实现。
    • 写失效: 当CPU写入一个缓存副本时,其它缓存中的副本将置为失效,这种方式能够确保CPU只能读写一个数据的副本,其它核心的副本都是无效的,这种手段也是现代CPU最常见的手段之一,MSI、MESI、MOSI、MOESI、MESIF协议都属于这类。
    • 写更新: 当CPU写入一个缓存副本时,其它缓存中的副本将会通过CPU内部总线进行更新,相当于数据更新的一次广播,这种手段会引起总线的流量增大,所以比较少见,Dragon、firefly协议属于这类。

说一说对MESI缓存一致性协议的理解

缓存一致性协议是一种缓存锁,目的是为了解决使用缓存后带来的可见性一致性问题。MESI协议只是协议其中的一种,Intel使用的缓存一致性协议就是MESI。一下以缓存行为单位,每个单位64字节,cpu多个核可以共同操作缓存。

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

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

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

  • Invalid:标记该缓存行被其他cpu修改过, 表示该缓存行中的内容时是无效的。 比如被Modified修改过。

详解MESI缓存一致性协议


缓存一致性协议会出现什么问题?

会出现伪共享的问题,因为读取缓存时以缓存行(Cache Line)为单位每个缓存行64字节。例如:有两个变量a和b在同一个缓存行中,有两个线程,线程1会修改a的值,线程b修改b的值。当线程1读取变量a时,会将b一起读取到cpu缓存中(因为是以缓存行为单位),线程1修改a值后,其他包含这个缓存行都会失效(标记为i)。当线程b想要修改b的值时,发现b所在的缓存行被表示为失效,需要重新去主存中读取。做这种无用功操作就是伪共享问题。



那伪共享问题应该如何解决

1.使用缓存对其

  • 缓存读取是以缓存行为单位,一个缓存行64字节。为了防止伪共享问题,我们只需要把缓存行沾满即可(空间换时间).
  • 以Long类型为例子,Long类型是8字节,那么占满是8个Long类型。我们只需要在a和b之间加上7个Long类型,a和b就不会在同一个缓存行了。

2.@Sum.misc.Contended 注解(JDK8) (-XX:RestrictContended)

注解可以使用在类上,也可以使用在变量上。


JMM理解

说一说并发编程的三大特性

原子性、有序性、一致性

  • 原子性:就是一个操作或多个操作中,要么全部执行,要么全部不执行。

    例如:账户A向账户B转账1000元,这个么过程涉及到两个操作,(1)A账户减去1000元 (2)B账户增加1000元。这么两个操作必须具备原子性。否则A账户钱少了,B账户没增加。

  • 有序性: 程序执行顺序按照代码先后顺序执行。

    处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致(指令重排),但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。(此处的结果一致指的是在单线程情况下)

    指令重排的理解:单线程侠,如果两个操作更换位置后,对后续操作结果没有影响,可以对这两个操作可以互换顺序。

  • 可见性: 可见性是指多线程共享一个变量,其中一个线程改变了变量值,其他线程能够立即看到修改的值。

    例如:

    //线程1执行的代码
    int i = 0;
    i = 10;
    //线程2执行的代码
    j = i;
    

    CPU1执行线程1代码,CPU执行线程2代码。CPU读取i=0到CPU缓存中,修改i=10到自己缓存,还没更新到主存,此时CPU2读取的i还是主存中i=0,此时j会被赋值为0;


说一说你对JMM的理解

  • JMM是java内存模型,从抽象的角度来看,JMM定义了线程和内存之间的抽象关系。线程之间的共享变量存储的主存中(main MeMory),每个线程都有自己的本地内存(Local Memory)。当更新数据时,会从主存中读取数据到本地内存后将内容更新,刚更新完再写到本地内存中,后续再刷到主存中。


说说JVM对Java内存模型(JMM)的实现

在JVM内部,JMM把内存分为两部分:线程栈区堆区。JVM运行过程中,每个线程都有自己的线程栈,线程栈包含了线程执行的方法相关信息,我们称为调用栈,是线程私有的。堆主要存储的是对象,线程共享的。

  • 局部变量: (1)对于基本数据类型的局部变量,会直接保存在栈中。他们的值是线程私有的,不会共享。(2)对于引用类型的局部变量:栈中保存的是对象的引用,对象实际存储在堆中。
  • 成员变量: 对于成员变量,无论是基本数据类型还是引用类型,会有存储到堆中。
  • static类型的变量:不管是基础数据类型还是引用类型,都直接存储在堆中。

可见性问题

JMM如何保证可见性?

volatile关键字,volatile可以保证可见性和有序性。

  • volatile 写:当写一个volatile变量时,JMM会将本地内存值立即刷回主存。
  • volatile 读:当读一个volatile变量时,JMM会将本地副本失效,去主存读。

volatile保证可见性在JMM层面原理

volatile修饰的共享变量在执行写操作后,会立即刷回到主存,以供其它线程读取到最新的记录。

volatile保证可见性在CPU层面原理

volatile关键字底层通过lock前缀指令,进行缓存一致性的缓存锁定方案,通过总线嗅探MESI协议来保证多核缓存的一致性问题,保证多个线程读取到最新内容。 lock前缀指令除了具有缓存锁定这样的原子操作,它还具有类似内存屏障的功能,能够保证指令重排的问题。


有序性问题

听说过重排序吗?说说你的理解

重排序分为编译器优化的重排序指令级并行的重排序内存系统的重排序。但是指令重排有一个规则,as-if-seiral不管怎么重排序,单线程的程序执行结果不能够被改变,编译器、处理器等都得遵循这个规范和准则。

  • 编译器优化的重排序:编译器在不改变单线程成语予以的前提下,能够重新安排执行顺序。
  • **指令集并行的重排序:**若不存在数据依赖,处理器能够处理器能够改变语句对应机器指令的执行顺序。
  • **内存系统重排序:**CPU为了提高执行效率,会在一条指令执行过程中(比如内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系。

cpu乱序执行的例子

可以看见,上面赋值输出,不管什么情都不会输出0,0.问题,但是最后0,0还是出现了,为什么?因为出现了重排序优化(重排序就是某条指令优先执行),避免重排序需要用到Volatile优化。


public class Disorder 
    private static int x = 0, y = 0;
    private static int a = 0, b =0;

    public static void main(String[] args) throws InterruptedException 
        int i = 0;
        for(;;) 
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread one = new Thread(new Runnable() 
                public void run() 
                    //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
                    //shortWait(100000);
                    a = 1;//a=1
                    x = b;//1可能是1或0
                
            );

            Thread other = new Thread(new Runnable() 
                public void run() 
                    b = 1;//b=1
                    y = a;//a可能是0或1
                
            );
            one.start();other.start();
            one.join();other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) 
                System.err.println(result);
                break;
             else 
                //System.out.println(result);
            
        
    


    public static void shortWait(long interval)
        long start = System.nanoTime();
        long end;
        do
            end = System.nanoTime();
        while(start + interval >= end);
    

x和y都等于0情况分析

这里处理器 A 和处理器 B 可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的脏数据刷新到内存中(A3,B3)。当以这种时序执行时,程序就可以得到 x = y = 0 的结果。



volatile关键字的作用是什么?

Java 中 volatile 关键字是一个类型修饰符。JDK 1.5 之后,对其语义进行了增强

  • 保证可见性:保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了共享变量的值,共享变量修改后的值对其他线程立即可见
  • 保证有序性:通过禁止编译器、CPU 指令重排序和部分 happens-before 规则,解决有序性问题,避免了指令重排
  • 不能保证原子性

JMM如何保证有序性?

Volatile关键字(JMM内存屏障),内存屏障也成为内存栏杆,是一个CPU指令,volatile修饰的变量,在读写操作前后都会进行屏障的插入来保证执行的顺序不被编译器等优化器锁重排序。

内存屏障的功能有两个:(1)阻止屏障两边的指令重排、(2)刷新处理器缓存(保证内存可见性)

其实JVM是屏蔽了不同处理器架构的差异,提供了统一化的内存屏障,在CPU硬件层面不同处理器架构有不同的内存屏障,例如X86架构的内存屏障有4种: ifence、sfence、mfence、lock前缀指令。

X86架构的内存屏障

cpu层面:使用cpu内存屏障(硬件)


Happens-Before

SR-133 提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。 与程序员密切相关的 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 关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。happens- before 的定义很微妙

重新认识 Java 内存模型(JMM)

通过学习《深入理解Java虚拟机》有关Java 内存模型的介绍,整理的学习笔记,供你参考。

文章目录

Java 内存模型定义

Java 内存模型 (Java Memory Model ),简称JMM屏蔽各种硬件和操作系统的内存访问的差异 性,以实现JAVA 程序在各个平台下都能达到一致的内存访问效果。

首先,引入《深入理解Java虚拟机》中的一张图,如下:

如图所示,在每一个线程中,都会有一块内部的工作内存(working memory)。这块内存保存了主内存共享数据的拷贝副本。

注意区分:JVM内存中有一块线程独享的内存空间 ----- 虚拟机栈,这里的的工作内存不是虚拟机栈。在Java线程中,不存在所谓的工作内存,它只是对CPU寄存器和高速缓存的抽象的描述。

主内存和工作内存

  • Java内存模型中规定了所有的变量都存储在主内存中。
  • 每条线程还有自己的工作内存,保存了该线程中使用的变量的主内存副本,线程对变量的所有操作(读取、赋值)都在工作内存中进行。
  • 不同的线程之间也无法直接直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
  • 主内存直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问的是工作内存。

CPU普及

做为Java程序员,应该都知道线程是CPU调度的最小单位,线程中的字节码指令最终都是在CPU中执行的。

为了“压榨”处理性能,达到“高并发”的效果,在CPU中添加了高速缓存(cache)为作为缓冲。

在执行任务时,CPU会先将运算所需要的数据copy到高速缓存中,让运算能够快速进行,当运算结束后,再将缓存中的结果刷回(flush back)主内存,这样CPU就不用等待主内存的读写操作了。

那么问题来了。每个处理器都有自己的高速缓存,同时又共同操作同一块内存,当多个处理器同时操作主内存时,可能导致数据不一致,这就是缓存一致性问题。

内存间交互操作

关于主内存和工作内存之间的交互协议说明:

内存交互操作有八种,虚拟机的实现保证每一个操作都是原子性的:

  1. lock(锁定):作用于主内存的变量,标识变量为线程独占状态
  2. unlock(解锁):作用于主内存的变量,释放一个处于锁定状态的变量,释放后的变量才可以被其他线程锁定
  3. read(读取):作用于主内存变量,从主内存中读取出后面load操作要用到的变量
  4. load(载入):作用于主内存中的变量,把刚才read的值放入工作内存的副本中
  5. use(使用):作用于工作内存中的变量,当线程执行某个字节码指令需要用到相应的变量时,把工作内存中的变量副本传给执行引擎
  6. assign(赋值):作用于工作内存中的变量,把一个从执行引擎中接受到的值放入工作内存的变量副本中
  7. store(存储):作用于工作内存中的变量,把工作内存中的变量送到主内存,给后续的write使用
  8. write(写入):作用于主内存中的变量,把store的工作内存中的变量值,写入主内存中

JMM对这八种指令的使用,制定了如下规则:

  • read和load、store和write必须顺序执行,而且两个指令绑定出现;就是说出现read就要有load
  • 不允许一个线程丢弃最近的assign操作,工作内存中的变量改变后,必须write同步到主内存
  • 不允许一个线程把没有发生assign操作的变量同步到主内存
  • 新的变量必须诞生于主内存,不允许工作内存使用一个没有初始化的变量;use、store操作变量之前,必须经过load和assign操作
  • 变量同一时刻只允许一个线程对其lock,该线程可以对该变量加锁多次,释放锁需要执行相同次数的unlock,lock和unlock要成对出现
  • 一个变量没有lock,不能unlock;并且一个线程不能unlock被其他线程锁住的变量
  • 执行unlock前,必须把工作内存中的变量同步到主内存中
  • 执行lock操作,需要清空工作内存(所有),并且需要使用该变量之前,要重新执行load和assign操作

对于volatile 型变量的特殊规则

对所有线程是立即可见的,对volatile变量所有的写操作都能够立即反映到其他线程之中。

换句话说,volatile变量在各个线程中是一致的。(正确)

基于以上论据,不能推出基于 volatile变量的运算在并发下是线程安全的。(需要注意)

原子性、可见性与有序性

见这篇介绍 链接:如何重新认识synchronized和volatile

先行发生(Happens-Before)原则

为什么有这个?

如果Java内存中的所有的有序性都靠 volatile 和synchronized,那么有很多操作都会变得啰嗦。

它是判断数据是否存在竞争,线程是否安全的非常有用的手段。通过这个原则,我们可以通过几条简单的规则一揽子解决并发环境下两个操作之间是否可能存在 冲突的所有问题。

下面是Java内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。 如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序:

  1. 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。 准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、 循环等结构。

  2. 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。 这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。

  3. volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。

  4. 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。

  5. 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、 Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  6. 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

  7. 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

  8. 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

JVM内存区域的JMM的区别

JVM内存区域是指JVM运行时将内存数据分区域存储,强调对内存空间的划分。

JAVA内存模型是JAVA语言在多线程并发情况下对于共享变量内存操作的规范:解决变量在多线程的可见性、原子性的问题。

参考资料:

《深入理解Java虚拟机》周志明 著

以上是关于java基础—java内存模型(JMM)CPU架构缓存一致性重排序JMM的实现JMM保证可见性有序性问题的详解的主要内容,如果未能解决你的问题,请参考以下文章

JAVA模型之一:JMM(JAVA内存模型)

Java——聊聊JUC中的Java内存模型(JMM)

Java——聊聊JUC中的Java内存模型(JMM)

Java——聊聊JUC中的Java内存模型(JMM)

Java——聊聊JUC中的Java内存模型(JMM)

基于JVM原理JMM模型和CPU缓存模型深入理解Java并发编程