Java内存模型分析



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

1、线程通信机制

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

1.1、共享内存

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

1.2、消息传递

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

1.3、共享内存与消息传递的区别

2、内存模型

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

2.1、结构抽象

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

  • 多核CPU与内存之间
  • Java多线程与主存之间
2.1.1、多核CPU与内存之间

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

2.1.2、Java多线程与主存之间

JMM规定了所有的变量都存储在主内存(Main Memory)中。

每个线程有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
如图:

2.2、重排序

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

2.2.1、数据依赖

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

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

2.2.2、as-if-serial语义

as-if-serial语义的意思是,所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守as-if-serial语义。注意as-if-serial只保证单线程环境,多线程环境下无效。

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

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

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

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

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

2.3、顺序一致性

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

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

2.3.1、特性
  • 一个线程中的所有操作必定按照程序的顺序来执行。
  • 所有的线程都只能看到一个单一的执行顺序,不管是否同步。
  • 每个操作都必须原子执行且立即对所有程序可见。
2.3.2、例子
  • 加了锁
  • 未加锁

2.4、多线程内存可见性-happens before

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

2.4.1、定义

《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.2、规则
  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.3、Happens-Before原则到底是如何解决变量间可见性问题的?

重排序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原则,它们所产生分割线的作用都是相同的。

2.5、内存屏障

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

2.5.1、作用

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

2.5.2、类型
屏障类型 指令示例 说明
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.3、内存屏障在Java中的体现
2.5.3.1、volatile
  • volatile读之后,所有变量读写操作都不会重排序到其前面。
  • volatile读之前,所有volatile读写操作都已完成。
  • volatile写之后,volatile变量读写操作都不会重排序到其前面。
  • volatile写之前,所有变量的读写操作都已完成。

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

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

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

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

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

2.5.3.4、锁

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避免伪共享

原文地址:https://www.cnblogs.com/clawhub/p/12019798.html

时间: 2024-07-29 08:22:12

Java内存模型分析的相关文章

java内存模型分析2

不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程.主内存和工作内存的交互关系如下图所示,和上图很类似. 这里的主内存.工作内存与Java内存区域的Java堆.栈.方法区不是同一层次内存划分.!!!! 这里的主内存.工作内存与Java内存区域的Java堆.栈.方法区不是同一层次内存划分.!!!! 3.2 内存间交互操作 关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存.如何从工作内存同步到主内存之间的实现细节,Java内存模型定

【死磕Java并发】-----Java内存模型之分析volatile

前篇博客[死磕Java并发]-–深入分析volatile的实现原理 中已经阐述了volatile的特性了: volatile可见性:对一个volatile的读,总可以看到对这个变量最终的写: volatile原子性:volatile对单个读/写具有原子性(32位Long.Double),但是复合操作除外,例如i++; JVM底层采用"内存屏障"来实现volatile语义 下面LZ就通过happens-before原则和volatile的内存语义两个方向介绍volatile. volat

转:【Java并发编程】之十六:深入Java内存模型——happen-before规则及其对DCL的分析(含代码)

转载请注明出处:http://blog.csdn.net/ns_code/article/details/17348313 happen-before规则介绍 Java语言中有一个"先行发生"(happen-before)的规则,它是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,"影响"包括修改了内存中共享变量的值.发送了消息.调用了方法等,它与时间上的先后发生基本没有

Java内存模型JMM简单分析

参考博文:http://blog.csdn.net/suifeng3051/article/details/52611310 http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html    http://www.cnblogs.com/dolphin0520/p/3613043.html 一.Java内存区域的划分 由于Java程序是交给JVM执行的,所以我们在谈Java内存区域分析的时候事实上是指JVM内存区域划分. 根

【Java并发编程】之十六:深入Java内存模型——happen-before规则及其对DCL的分析(含代码)

happen-before规则介绍 Java语言中有一个"先行发生"(happen-before)的规则,它是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,"影响"包括修改了内存中共享变量的值.发送了消息.调用了方法等,它与时间上的先后发生基本没有太大关系.这个原则特别重要,它是判断数据是否存在竞争.线程是否安全的主要依据. 举例来说,假设存在如下三个线程,分别执行对应

Java内存模型与垃圾回收

1.Java内存模型 Java虚拟机在执行程序时把它管理的内存分为若干数据区域,这些数据区域分布情况如下图所示: 程序计数器:一块较小内存区域,指向当前所执行的字节码.如果线程正在执行一个Java方法,这个计数器记录正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计算器值为空. Java虚拟机栈:线程私有的,其生命周期和线程一致,每个方法执行时都会创建一个栈帧用于存储局部变量表.操作数栈.动态链接.方法出口等信息. 本地方法栈:与虚拟机栈功能类似,只不过虚拟机栈为虚拟机执行J

深入java内存模型(一)

最近本来想深入学习一下java线程,很想知道其中实现的原理,比如线程资源的共享,线程私有空间,以及线程直接的同步控制等.如果能了解它的实现,对于深入学习线程,会有很大的帮助.最近正在看一份<深入java内存模型>的资料.讲的就是java线程方面的实现原理,拿出来分享一下. 说到线程,我们首先想到的是线程的通信.学习操作系统时,线程通信有两种,一种是通过共享内存,另一种是通过消息传递.共享内存属于隐式的通信,线程之间共享一块内存,线程都可以往这块内存写数据,读数据就实现了通信.而消息传递时一个显

Java内存模型知识点小结---《深入理解Java内存模型》(程晓明)读书总结

一.Java内存模型介绍 内存模型的作用范围: 在Java中,所有实例域.静态域和数组元素存放在堆内存中,线程之间共享,下文称之为“共享变量”.局部变量.方法参数.异常处理器等不会在线程之间共享,不存在内存可见性问题,也不受内存模型的影响. 重排序与可见性: 现代编译器在编译源码时会做一些优化处理,对代码指令进行重排序:现代流水线结构的处理器为了提高并行度,在执行时也可能对指令做一些顺序上的调整.重排序包括编译器重排序.指令级并行重排序和内存系统重排序等.一般来说,编译器和处理器在做重排序的时候

java内存模型一

Java平台自动集成了线程以及多处理器技术,这种集成程度比Java以前诞生的计算机语言要厉害很多,该语言针对多种异构平台的平台独立性而使用的多线程技术支持也是具有开拓性的一面,有时候在开发Java同步和线程安全要求很严格的程序时,往往容易混淆的一个概念就是内存模型.究竟什么是内存模型?内存模型描述了程序中各个变量(实例域.静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节,对象最终是存储在内存里面的,这点没有错,但是编译器.运行库.处理器或者系统