java内存模型

作者: 美团Java | 来源:发表于2016-07-05 14:03 被阅读16683次

    简书 占小狼
    转载请注明原创出处,谢谢!

    java并发采用的是共享内存模型,线程之间的通信对程序员来说是透明的,内存可见性问题很容易困扰着java程序员,今天我们就来揭开java内存模型的神秘面纱。


    在揭开面纱之前,我们需要认识几个基础概念:内存屏障(memory Barriers),指令重排序,happens-before规则,as-if-serial语义。

    什么是 Memory Barrier(内存屏障)?

    内存屏障,又称内存栅栏,是一个CPU指令,基本上它是一条这样的指令:
    1、保证特定操作的执行顺序。
    2、影响某些数据(或则是某条指令的执行结果)的内存可见性。

    编译器和CPU能够重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。

    Memory Barrier所做的另外一件事是强制刷出各种CPU cache,如一个 Write-Barrier(写入屏障)将刷出所有在 Barrier 之前写入 cache 的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本。


    Memory Barrier.png

    这和java有什么关系?volatile是基于Memory Barrier实现的。

    如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。


    volatile.png

    这意味着,如果写入一个volatile变量a,可以保证:
    1、一个线程写入变量a后,任何线程访问该变量都会拿到最新值。
    2、在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。

    happens-before

    从jdk5开始,java使用新的JSR-133内存模型,基于happens-before的概念来阐述操作之间的内存可见性。

    在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。

    与程序员密切相关的happens-before规则如下:
    1、程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。
    2、监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。
    3、volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
    4、传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

    注意:两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。

    指令重排序

    在执行程序时,为了提高性能,编译器和处理器会对指令做重排序。但是,JMM确保在不同的编译器和不同的处理器平台之上,通过插入特定类型的Memory Barrier来禁止特定类型的编译器重排序和处理器重排序,为上层提供一致的内存可见性保证。

    1、编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
    2、指令级并行的重排序:如果不存l在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
    3、内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

    数据依赖性

    如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性。
    编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序。

    as-if-serial

    不管怎么重排序,单线程下的执行结果不能被改变,编译器、runtime和处理器都必须遵守as-if-serial语义。

    抽象结构

    java线程之间的通信由java内存模型(JMM)控制,JMM决定一个线程对共享变量(实例域、静态域和数组)的写入何时对其它线程可见。

    从抽象的角度来看,JMM定义了线程和主内存Main Memory(堆内存)之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有自己的本地内存Local Memory(只是一个抽象概念,物理上不存在),存储了该线程的共享变量副本。

    所以,线程A和线程B之前需要通信的话,必须经过一下两个步骤:
    1、线程A把本地内存中更新过的共享变量刷新到主内存中。
    2、线程B到主内存中读取线程A之前更新过的共享变量。

    END。
    我是占小狼。
    在魔都艰苦奋斗,白天是上班族,晚上是知识服务工作者。
    读完我的文章有收获,记得关注和点赞哦,如果非要打赏,我也是不会拒绝的啦!

    相关文章

      网友评论

      • 傻黑2018:为什么要有主内存和工作内存之分。
        超哥7:为了效率
      • 303cc8e5e30a:【两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。】 博主能详细解释下这句话吗? 既然两个操作之前存在hb关系,那必定是前一个操作先执行啊,否则结果怎么对后一个操作可见呢?难不成存在后面操作执行的时候等待前一个操作结果的情况?
        a3b65c108416:@没有_6e03 当两个操作没有数据依赖的时候,内存模型允许这种重排序。(因为没有依赖,所以也不存在什么可见与不可见了)
      • dd2f56d130c2:不错不错,收藏了。

        推荐下,源码圈 300 胖友的书单整理:http://t.cn/R0Uflld


      • HelloBobi:注意:两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。

        其中的“,且前一个操作按顺序排在后一个操作之前。” 如何理解??
        303cc8e5e30a:@柳叶君123 我也是没理解这句话的意思,感觉自相矛盾啊,你说的重新排序我感觉不对吧,既然两个操作之前存在hb,就应该禁止重新排序吧
        柳叶君123:@柯_8835 可以这么理解:物理上按照顺序排下来的两条指令第一个对第二个可见,但是不是一定第一条先执行呢,不一定,因为可能发生重拍,顺序上第一条不一定先执行
        柯_8835:“”并不意味前一个操作必须要在后一个操作之前执行”
        然后,
        “”且前一个操作按顺序排在后一个操作之前“”,不好理解啊
      • 迷失于重逢:还是应该有个参考来源。
        美团Java:@365ark 一开始只是自己做总结的:joy:
      • 09793c784dd0:怎么感觉很多事深入理解java虚拟机里面的原话?
      • 小乐xing:http://images.cnblogs.com/cnblogs_com/aigongsi/201204/201204011757234696.jpg
        给你找了个图,分析volatile,在load use asign 都不用副本的,都取主内存最新的 :sweat_smile:
        美团Java:@Godson_a837 不是,缓存已经没有了
        77270532c100:你的意思是还是从缓存中读取了?
        文曜繁星:@小乐xing volatile中,其中一个线程对产量修改地话,会直接让其他cpu缓存中的变量失效,这样就会从主内存中重新回去数据。
      • 2fc176aaa8ea:请问下那个memory barrier的配图是哪儿来的,听好看的,谢谢啦
        美团Java:@2fc176aaa8ea 这个图片是国外博客贴过来的
      • leeyaf:不错不错

      本文标题:java内存模型

      本文链接:https://www.haomeiwen.com/subject/uaqfjttx.html