标签:机制 管程 art 复杂 img pen 插入 mode reload
在学习Java内存模型之前,先了解一下线程通信机制。
在并发编程中,线程之间相互交换信息就是线程通信。目前有两种机制:内存共享与消息传递。
Java采用的就是共享内存,本次学习的主要内容就是这个内存模型。
内存共享方式必须通过锁或者CAS技术来获取或者修改共享的变量,看起来比较简单,但是锁的使用难度比较大,业务复杂的话还有可能发生死锁。
Actor模型即是一个异步的、非阻塞的消息传递机制。Akka是对于Java的Actor模型库,用于构建高并发、分布式、可容错、事件驱动的基于JVM的应用。
消息传递方式就是显示的通过发送消息来进行线程间通信,对于大型复杂的系统,可能优势更足。
Java既然使用内存共享,必然就涉及到内存模型。
内存模型结构的抽象分为两个层次:
因为CPU的运行速度与内存之间的存取速度不成正比,所以,引入了多级缓存概念,相应的也引出了缓存读取不一致问题,当然缓存一致性协议解决了这个问题(本文不深入讨论)。
结构抽象如图:
JMM规定了所有的变量都存储在主内存(Main Memory)中。
每个线程有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
如图:
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
例如,如果一个线程更新字段 A的值,然后更新字段B的值,而且字段B 的值不依赖于字段A 的值,那么,处理器就能够自由的调整它们的执行顺序,而且缓冲区能够在更新字段 A之前更新字段 B的值到主内存。
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。
如图所示,A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。
as-if-serial语义的意思是,所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守as-if-serial语义。注意as-if-serial只保证单线程环境,多线程环境下无效。
as-if-serial语义使得重排序不会干扰单线程程序,也无需担心内存可见性问题。
从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序。
重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。
顺序一致性是多线程环境下的理论参考模型,为程序提供了极强的内存可见性保证,在顺序一致性执行过程中,所有动作之间的先后关系与程序代码的顺序一致。
JMM对正确同步的多线程程序的内存一致性做出的保证:
如果程序是正确同步的,程序的执行将具有顺序一致性(sequentially consistent)。
在并发编程时,会碰到一个难题:即一个操作A的结果对另一个操作B可见,即多线程变量可见性问题。
解决方法就是提出了happens-before概念,即一个操作A与另一个操作B存在happens-before关系。
《Time,Clocks and the Ordering of Events in a Distributed System》点击查看论文。
前提:操作A happens-before 操作B。
对于第一条,编码时,A操作在B操作之前,则执行顺序就是A之后B。
对于第二条,如果重排序后,虽然执行顺序不是A到B,但是最终A的结果对B可见,则允许这种重排序。
重排序
和CPU高速缓存
有利于计算机性能的提高,但却对多CPU处理的一致性带来了影响。为了解决这个矛盾,我们可以采取一种折中的办法。我们用分割线把整个程序划分成几个程序块,在每个程序块内部的指令是可以重排序的,但是分割线上的指令与程序块的其它指令之间是不可以重排序的。在一个程序块内部,CPU不用每次都与主内存进行交互,只需要在CPU缓存中执行读写操作即可,但是当程序执行到分割线处,CPU必须将执行结果同步到主内存或从主内存读取最新的变量值。那么,Happens-Before规则就是定义了这些程序块的分割线。下图展示了一个使用锁定原则作为分割线的例子:
如图所示,这里的unlock M和lock M就是划分程序的分割线。在这里,红色区域和绿色区域的代码内部是可以进行重排序的,但是unlock和lock操作是不能与它们进行重排序的。即第一个图中的红色部分必须要在unlock M指令之前全部执行完,第二个图中的绿色部分必须全部在lock M指令之后执行。并且在第一个图中的unlock M指令处,红色部分的执行结果要全部刷新到主存中,在第二个图中的lock M指令处,绿色部分用到的变量都要从主存中重新读取。
在程序中加入分割线将其划分成多个程序块,虽然在程序块内部代码仍然可能被重排序,但是保证了程序代码在宏观上是有序的。并且可以确保在分割线处,CPU一定会和主内存进行交互。Happens-Before原则就是定义了程序中什么样的代码可以作为分隔线。并且无论是哪条Happens-Before原则,它们所产生分割线的作用都是相同的。
内存屏障是为了解决在cacheline上的操作重排序问题。
强制CPU将store buffer中的内容写入到 cacheline中。
强制CPU将invalidate queue中的请求处理完毕。
屏障类型 | 指令示例 | 说明 |
---|---|---|
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.
根据JMM规则,结合内存屏障的相关分析:
在CPU架构中依靠lock信号保证可见性并禁止重排序。
lock前缀是一个特殊的信号,执行过程如下:
因此,lock信号虽然不是内存屏障,但具有mfence的语义(当然,还有排他性的语义)。
与内存屏障相比,lock信号要额外对总线和缓存上锁,成本更高。
JVM的内置锁通过操作系统的管程实现。由于管程是一种互斥资源,修改互斥资源至少需要一个CAS操作。因此,锁必然也使用了lock信号,具有mfence的语义。
《Java并发编程的艺术》一一3.2 重排序
啃碎并发(11):内存模型之重排序
【细谈Java并发】内存模型之重排序
【死磕Java并发】-----Java内存模型之重排序
https://www.cnblogs.com/chenssy/p/6393321.html
https://segmentfault.com/a/1190000011458941
https://blog.csdn.net/liu_dong_liang/article/details/80391040
一文解决内存屏障
内存屏障与 JVM 并发
内存屏障和 volatile 语义
Java内存模型Cookbook(二)内存屏障
谈乱序执行和内存屏障
内存屏障
深入理解 Java 内存模型(六)——final
伪共享(FalseSharing)
避免并发现线程之间的假共享
伪共享(FalseSharing)和缓存行(CacheLine)大杂烩
伪共享(falsesharing),并发编程无声的性能杀手
Java8使用@sun.misc.Contended避免伪共享
标签:机制 管程 art 复杂 img pen 插入 mode reload
原文地址:https://www.cnblogs.com/clawhub/p/12019798.html