Java内存模型分析


在学习Java内存模型以前,先了解一下线程通讯机制。html

一、线程通讯机制

在并发编程中,线程之间相互交换信息就是线程通讯。目前有两种机制:内存共享与消息传递。java

1.一、共享内存

Java采用的就是共享内存,本次学习的主要内容就是这个内存模型。 内存共享方式必须经过锁或者CAS技术来获取或者修改共享的变量,看起来比较简单,可是锁的使用难度比较大,业务复杂的话还有可能发生死锁。git

1.二、消息传递

Actor模型便是一个异步的、非阻塞的消息传递机制。Akka是对于Java的Actor模型库,用于构建高并发、分布式、可容错、事件驱动的基于JVM的应用。 消息传递方式就是显示的经过发送消息来进行线程间通讯,对于大型复杂的系统,可能优点更足。github

1.三、共享内存与消息传递的区别

线程通讯机制区别.png

二、内存模型

Java既然使用内存共享,必然就涉及到内存模型。web

2.一、结构抽象

内存模型结构的抽象分为两个层次:编程

  • 多核CPU与内存之间
  • Java多线程与主存之间
2.1.一、多核CPU与内存之间

由于CPU的运行速度与内存之间的存取速度不成正比,因此,引入了多级缓存概念,相应的也引出了缓存读取不一致问题,固然缓存一致性协议解决了这个问题(本文不深刻讨论)。 结构抽象如图:segmentfault

多核处理器和内存交互.jpg

2.1.二、Java多线程与主存之间

JMM规定了全部的变量都存储在主内存(Main Memory)中。缓存

每一个线程有本身的工做内存(Working Memory),线程的工做内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的全部操做(读取、赋值等)都必须在工做内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工做内存的拷贝,可是因为它特殊的操做顺序性规定,因此看起来如同直接在主内存中读写访问通常)。不一样的线程之间也没法直接访问对方工做内存中的变量,线程之间值的传递都须要经过主内存来完成。 如图: JAVA内存模型抽象.jpg多线程

JAVA内存模型抽象示意图.png

2.二、重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行从新排序的一种手段。 例如,若是一个线程更新字段 A的值,而后更新字段B的值,并且字段B 的值不依赖于字段A 的值,那么,处理器就可以自由的调整它们的执行顺序,并且缓冲区可以在更新字段 A以前更新字段 B的值到主内存。架构

2.2.一、数据依赖

若是两个操做访问同一个变量,且这两个操做中有一个为写操做,此时这两个操做之间就存在数据依赖性。

数据依赖.png

如图所示,A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。所以在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器能够重排序A和B之间的执行顺序。

2.2.二、as-if-serial语义

as-if-serial语义的意思是,全部的操做都可觉得了优化而被重排序,可是你必需要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵照as-if-serial语义。注意as-if-serial只保证单线程环境,多线程环境下无效。

as-if-serial语义使得重排序不会干扰单线程程序,也无需担忧内存可见性问题。

2.2.三、重排序类型
  1. 编译器优化的重排序 编译器在不改变单线程程序语义的前提下,能够从新安排语义。
  2. 指令级并行的重排序 代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。若是不存在数据依赖性,处理器能够改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序 因为处理器使用缓存和读/写缓冲区,这使得加载和存储操做看上去多是在乱序执行。

从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序。

reorder.jpg

2.2.四、禁止重排序
  • 只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语义,这种重排序就会被编译器排序规则和处理器内存屏障插入策略禁止。
  • Java内存模型的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,经过内存屏障指令来禁止特定类型的处理器重排序。
  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操做之间不能重排序。(防止拿到对象时,final域还未赋值);初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操做之间不能重排序。
2.2.五、重排序对多线程的影响

重排序不会影响单线程环境的执行结果,可是会破坏多线程的执行语义。

2.三、顺序一致性

顺序一致性是多线程环境下的理论参考模型,为程序提供了极强的内存可见性保证,在顺序一致性执行过程当中,全部动做之间的前后关系与程序代码的顺序一致。

JMM对正确同步的多线程程序的内存一致性作出的保证: 若是程序是正确同步的,程序的执行将具备顺序一致性(sequentially consistent)。

2.3.一、特性
  • 一个线程中的全部操做一定按照程序的顺序来执行。
  • 全部的线程都只能看到一个单一的执行顺序,不论是否同步。
  • 每一个操做都必须原子执行且当即对全部程序可见。
2.3.二、例子
  • 加了锁 顺序一致性-加锁.png
  • 未加锁 顺序一致性-未加锁.png

2.四、多线程内存可见性-happens before

在并发编程时,会碰到一个难题:即一个操做A的结果对另外一个操做B可见,即多线程变量可见性问题。 解决方法就是提出了happens-before概念,即一个操做A与另外一个操做B存在happens-before关系。

2.4.一、定义

《Time,Clocks and the Ordering of Events in a Distributed System》点击查看论文。

  • 若是一个操做happens-before另外一个操做,那么第一个操做的执行结果将对第二个操做可见,并且第一个操做的执行顺序排在第二个操做以前。
  • 两个操做之间存在happens-before关系,并不意味着必定要按照happens-before原则制定的顺序来执行。若是重排序以后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

前提:操做A happens-before 操做B。 对于第一条,编码时,A操做在B操做以前,则执行顺序就是A以后B。 对于第二条,若是重排序后,虽然执行顺序不是A到B,可是最终A的结果对B可见,则容许这种重排序。

2.4.二、规则
  1. 程序次序规则: 一个线程内,按照代码顺序,书写在前面的操做先行发生于书写在后面的操做,这个规则只对单线程有效,在多线程环境下没法保证正确性。
  2. 锁定规则: 无论单线程多线程,一个unLock操做先行发生于后面对同一个锁的lock操做。
  3. volatile变量规则: 它标志着volatile保证了线程可见性。通俗点讲就是若是一个线程先去写一个volatile变量,而后另外一个线程去读这个变量,那么这个写操做必定是happens-before读操做的。
  4. 传递规则: 若是操做A先行发生于操做B,而操做B又先行发生于操做C,则能够得出操做A先行发生于操做C;
  5. 线程启动规则: 假定线程A在执行过程当中,经过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。即:调用start方法时,会将start方法以前全部操做的结果同步到主内存中,新线程建立好后,须要从主内存获取数据。这样在start方法调用以前的全部操做结果对于新建立的线程都是可见的。
  6. 线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  7. 线程终结规则: 线程中全部的操做都先行发生于线程的终止检测,咱们能够经过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则: 一个对象的初始化完成先行发生于他的finalize()方法的开始;
2.4.三、Happens-Before原则究竟是如何解决变量间可见性问题的?

重排序CPU高速缓存有利于计算机性能的提升,但却对多CPU处理的一致性带来了影响。为了解决这个矛盾,咱们能够采起一种折中的办法。咱们用分割线把整个程序划分红几个程序块,在每一个程序块内部的指令是能够重排序的,可是分割线上的指令与程序块的其它指令之间是不能够重排序的。在一个程序块内部,CPU不用每次都与主内存进行交互,只须要在CPU缓存中执行读写操做便可,可是当程序执行到分割线处,CPU必须将执行结果同步到主内存或从主内存读取最新的变量值。那么,Happens-Before规则就是定义了这些程序块的分割线。下图展现了一个使用锁定原则做为分割线的例子: happens-before.jpg

如图所示,这里的unlock M和lock M就是划分程序的分割线。在这里,红色区域和绿色区域的代码内部是能够进行重排序的,可是unlock和lock操做是不能与它们进行重排序的。即第一个图中的红色部分必需要在unlock M指令以前所有执行完,第二个图中的绿色部分必须所有在lock M指令以后执行。而且在第一个图中的unlock M指令处,红色部分的执行结果要所有刷新到主存中,在第二个图中的lock M指令处,绿色部分用到的变量都要从主存中从新读取。 在程序中加入分割线将其划分红多个程序块,虽然在程序块内部代码仍然可能被重排序,可是保证了程序代码在宏观上是有序的。而且能够确保在分割线处,CPU必定会和主内存进行交互。Happens-Before原则就是定义了程序中什么样的代码能够做为分隔线。而且不管是哪条Happens-Before原则,它们所产生分割线的做用都是相同的。

2.五、内存屏障

内存屏障是为了解决在cacheline上的操做重排序问题。

2.5.一、做用

强制CPU将store buffer中的内容写入到 cacheline中。 强制CPU将invalidate queue中的请求处理完毕。

2.5.二、类型
屏障类型 指令示例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 该屏障确保Load1数据的装载先于Load2及其后全部装载指令的的操做
StoreStore Barriers Store1;StoreStore;Store2 该屏障确保Store1马上刷新数据到内存(使其对其余处理器可见)的操做先于Store2及其后全部存储指令的操做
LoadStore Barriers Load1;LoadStore;Store2 确保Load1的数据装载先于Store2及其后全部的存储指令刷新数据到内存的操做
StoreLoad Barriers Store1;StoreLoad;Load1 该屏障确保Store1马上刷新数据到内存的操做先于Load2及其后全部装载装载指令的操做.它会使该屏障以前的全部内存访问指令(存储指令和访问指令)完成以后,才执行该屏障以后的内存访问指令

StoreLoad Barriers同时具有其余三个屏障的效果,所以也称之为全能屏障,是目前大多数处理器所支持的,可是相对其余屏障,该屏障的开销相对昂贵.在x86架构的处理器的指令集中,lock指令能够触发StoreLoad Barriers.

2.5.三、内存屏障在Java中的体现
2.5.3.一、volatile
  • volatile读以后,全部变量读写操做都不会重排序到其前面。
  • volatile读以前,全部volatile读写操做都已完成。
  • volatile写以后,volatile变量读写操做都不会重排序到其前面。
  • volatile写以前,全部变量的读写操做都已完成。

根据JMM规则,结合内存屏障的相关分析:

  • 在每个volatile写操做前面插入一个StoreStore屏障。这确保了在进行volatile写以前前面的全部普通的写操做都已经刷新到了内存。
  • 在每个volatile写操做后面插入一个StoreLoad屏障。这样能够避免volatile写操做与后面可能存在的volatile读写操做发生重排序。
  • 在每个volatile读操做后面插入一个LoadLoad屏障。这样能够避免volatile读操做和后面普通的读操做进行重排序。
  • 在每个volatile读操做后面插入一个LoadStore屏障。这样能够避免volatile读操做和后面普通的写操做进行重排序。
2.5.3.二、final:
  • 写 final 域的重排序规则 JMM 禁止编译器把 final 域的写重排序到构造函数以外。 编译器会在 final 域的写以后,构造函数 return 以前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数以外。
  • 读 final 域的重排序规则 在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操做(注意,这个规则仅仅针对处理器)。编译器会在读 final 域操做的前面插入一个 LoadLoad 屏障。
2.5.3.三、CAS

在CPU架构中依靠lock信号保证可见性并禁止重排序。 lock前缀是一个特殊的信号,执行过程以下:

  • 对总线和缓存上锁。
  • 强制全部lock信号以前的指令,都在此以前被执行,并同步相关缓存。
  • 执行lock后的指令(如cmpxchg)。
  • 释放对总线和缓存上的锁。
  • 强制全部lock信号以后的指令,都在此以后被执行,并同步相关缓存。

所以,lock信号虽然不是内存屏障,但具备mfence的语义(固然,还有排他性的语义)。 与内存屏障相比,lock信号要额外对总线和缓存上锁,成本更高。

2.5.3.四、锁

JVM的内置锁经过操做系统的管程实现。因为管程是一种互斥资源,修改互斥资源至少须要一个CAS操做。所以,锁必然也使用了lock信号,具备mfence的语义。

参考

《Java并发编程的艺术》一一3.2 重排序 啃碎并发(11):内存模型之重排序 【细谈Java并发】内存模型之重排序 【死磕Java并发】-----Java内存模型之重排序 http://www.javashuo.com/article/p-ooicwjrf-cg.html http://www.javashuo.com/article/p-gyfgocgd-hy.html http://www.javashuo.com/article/p-aaxpbbwh-mv.html 一文解决内存屏障 内存屏障与 JVM 并发 内存屏障和 volatile 语义 Java内存模型Cookbook(二)内存屏障 谈乱序执行和内存屏障 内存屏障 深刻理解 Java 内存模型(六)——final 伪共享(FalseSharing) 避免并发现线程之间的假共享 伪共享(FalseSharing)和缓存行(CacheLine)大杂烩 伪共享(falsesharing),并发编程无声的性能杀手 Java8使用@sun.misc.Contended避免伪共享

tencent.jpg

相关文章
相关标签/搜索