金水郑州网站建设,电脑版qq,wordpress安装不,个人网站可以收费吗文章目录 一、概念区分1、什么是内存模型#xff1f;什么是#xff08;内存区域#xff09;运行时数据区#xff1f;2、为什么要有Java内存模型#xff1f;2.1、硬件的效率与一致性2.2、 CPU和缓存的一致性2.2.1、为什么需要CPU cache#xff1f;2.2.2、三级缓存#xf… 文章目录 一、概念区分1、什么是内存模型什么是内存区域运行时数据区2、为什么要有Java内存模型2.1、硬件的效率与一致性2.2、 CPU和缓存的一致性2.2.1、为什么需要CPU cache2.2.2、三级缓存L1、L2、L3 2.3、 乱序执行优化 二、JMM- Java内存模型2.1、内存模型组成及抽象示意图2.2、内存交互的基本操作2.3、Java内存模型的运行规则1、八大操作的同步规则2、内存交互的3个特性原子性、可见性、有序性volatile的可见性、有序性synchronized的原子性、可见性、有序性 3、重排序4、JMM的重排序屏障数据依赖性as-if-serial语义重排序对多线程的影响 5、happens-before 先行发生原则final变量的特殊规则long 、doube类型的变量的特殊规则 三、JVM运行时数据区Java内存区域 一、概念区分
1、什么是内存模型什么是内存区域运行时数据区
Java的内存区域和内存模型是不一样的东西内存区域是指 JVM 运行时将数据分区域存储强调对内存空间的划分。
而内存模型Java Memory Model简称 JMM 是定义了线程和主内存之间的抽象关系即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式如果我们要想深入了解Java并发编程就要先理解好Java内存模型。
2、为什么要有Java内存模型
在现代多核处理器中每个处理器都有自己的缓存需要定期的与主内存进行协调。想要确保每个处理器在任意时刻知道其他处理器正在进行的工作将需要很大的开销且通常是没必要的。
2.1、硬件的效率与一致性
1、 由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存cache来作为内存与处理器之间的缓冲将运算需要使用到的数据复制到缓存中让运算能快速进行当运算结束后再从缓存同步回内存之中这样一来处理器就无需等待缓慢的内存读写了。 2、基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾但是引入了一个新的问题缓存一致性Cache Coherence。在多处理器系统中每个处理器都有自己的高速缓存而他们又共享同一主存。如下图所示 多个处理器运算任务都涉及同一块主存需要一种协议可以保障数据的一致性这类协议有MSI、MESI、MOSI及Dragon Protocol等。Java虚拟机内存模型中定义的内存访问操作与硬件的缓存访问操作是具有可比性的。看下面第二点介绍
3、除此之外为了使得处理器内部的运算单元能尽可能被充分利用处理器可能会对输入代码进行乱序执行Out-Of-Order Execution优化处理器会在计算之后将对乱序执行的代码进行结果重组保证结果准确性。与处理器的乱序执行优化类似Java虚拟机的即时编译器中也有类似的指令重排序Instruction Recorder优化。看下面第三点介绍。
2.2、 CPU和缓存的一致性
2.2.1、为什么需要CPU cache
因为CPU的频率太快了快到主存跟不上这样在处理器时钟周期内CPU常常需要等待主存浪费资源。CPU往往需要重复处理相同的数据、重复执行相同的指令如果这部分数据、指令CPU能在CPU缓存中找到CPU就不需要从内存或硬盘中再读取数据、指令从而减少了整机的响应时间所以cache的出现是为了缓解CPU和内存之间速度的不匹配问题 (结构cpu - cache - memory) 在程序执行的过程中就变成了
当程序在运行过程中会将运算需要的数据从主存复制一份到CPU的高速缓存当中
那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据
当运算结束之后再将高速缓存中的数据刷新到主存当中。在Intel官网上产品-处理器界面内对缓存的定义为CPU高速缓存是处理器上的一个快速记忆区域。英特尔智能高速缓存SmartCache是指可让所有内核动态共享最后一级高速缓存的架构。这里就提及到了最后一级高速缓存的概念即为CPU缓存中的L3(三级缓存)那么我们继续来解释一下什么叫三级缓存分别又是指哪三级缓存。 2.2.2、三级缓存L1、L2、L3 三级缓存L1一级缓存、L2二级缓存、L3三级缓存都是集成在CPU内的缓存它们的作用都是作为CPU与主内存之间的高速数据缓冲区L1最靠近CPU核心L2其次L3再次 运行速度方面L1最快、L2次快、L3最慢 容量大小方面L1最小、L2较大、L3最大CPU会先在最快的L1中寻找需要的数据找不到再去找次快的L2还找不到再去找L3L3都没有那就只能去内存找了。单核CPU只含有一套L1L2L3缓存如果CPU含有多个核心即多核CPU则每个核心都含有一套L1甚至和L2缓存而共享L3或者和L2缓存。 单CPU双核的缓存结构 在单线程环境下cpu核心的缓存只被一个线程访问。缓存独占不会出现访问冲突等问题 在多线程场景下在CPU和主存之间增加缓存就可能存在缓存一致性问题也就是说在多核CPU中每个核的自己的缓存中关于同一个数据的缓存内容可能不一致这也就是我们上面提到的缓存一致性的问题 2.3、 乱序执行优化
从java源码到最终实际执行的指令序列会经历下面3种重排序 举个重排序的例子
a10,ba 这一组 b依赖a不会重排序a10,b50 这一组 a和b 没有关系那么就有可能被重排序执行 b50,a10 cpu和编译器为了提高程序的执行效率会按照一定的规则允许指令优化不影响单线程程序执行结果但是多线程就会影响程序结果
二、JMM- Java内存模型 Java内存模型即Java Memory Model简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型所以JMM是隶属于JVM的。
Java内存模型Java Memory Model ,JMM就是一种符合内存模型规范的屏蔽了各种硬件和操作系统的访问差异的保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。可以避免像c等直接使用物理硬件和操作系统的内存模型在不同操作系统和硬件平台下表现不同比如有些c/c程序可能在windows平台运行正常而在linux平台却运行有问题。这即使得Java程序能够 “一次编写到处运行”。 Java内存模型是共享内存的并发模型线程之间主要通过读-写共享变量堆内存中的实例域静态域和数组元素来完成隐式通信。 Java 内存模型JMM控制 Java 线程之间的通信决定一个线程对共享变量的写入何时对另一个线程可见。 2.1、内存模型组成及抽象示意图
主内存Java内存模型规定了所有变量都存储在主内存(Main Memory)中此处的主内存与介绍物理硬件的主内存名字一样两者可以互相类比但此处仅是虚拟机内存的一部分。
工作内存每条线程都有自己的工作内存(Working Memory又称本地内存可与前面介绍的处理器高速缓存类比)线程的工作内存中保存了该线程使用到的变量的主内存中的共享变量的副本拷贝。工作内存是 JMM 的一个抽象概念并不真实存在。它涵盖了缓存写缓冲区寄存器以及其他的硬件和编译器优化。 从上图来看线程A与线程B之间如要通信的话必须要经历下面2个步骤
线程A把本地内存A中更新过的共享变量刷新到主内存中去。线程B到主内存中去读取线程A之前已更新过的共享变量。
拿图举例 这样才完成了上面的线程对下面线程变量的访问。
2.2、内存交互的基本操作
关于主内存与工作内存之间的具体交互协议即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节Java内存模型中定义了下面介绍8种操作来完成。
lock锁定 作用于主内存的变量把一个变量标识为一条线程独占状态。unlock解锁 作用于主内存变量把一个处于锁定状态的变量释放出来释放后的变量才可以被其他线程锁定。read读取 作用于主内存变量把一个变量值从主内存传输到线程的工作内存中以便随后的load动作使用load载入 作用于工作内存的变量它把read操作从主内存中得到的变量值放入工作内存的变量副本中。use使用 作用于工作内存的变量把工作内存中的一个变量值传递给执行引擎每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。assign赋值 作用于工作内存的变量它把一个从执行引擎接收到的值赋值给工作内存的变量每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。store存储 作用于工作内存的变量把工作内存中的一个变量的值传送到主内存中以便随后的write的操作。write写入 作用于主内存的变量它把store操作从工作内存中一个变量的值传送到主内存的变量中。
2.3、Java内存模型的运行规则
1、八大操作的同步规则
JMM在执行前面介绍8种基本操作时为了保证内存间数据一致性JMM中规定需要满足以下规则 规则1如果要把一个变量从主内存中复制到工作内存就需要按顺序的执行 read 和 load 操作如果把变量从工作内存中同步回主内存中就要按顺序的执行 store 和 write 操作。但 Java 内存模型只要求上述操作必须按顺序执行而没有保证必须是连续执行。 规则2不允许 read 和 load、store 和 write 操作之一单独出现。 规则3不允许一个线程丢弃它的最近 assign 的操作即变量在工作内存中改变了之后必须同步到主内存中。 规则4不允许一个线程无原因的没有发生过任何 assign 操作把数据从工作内存同步回主内存中。 规则5一个新的变量只能在主内存中诞生不允许在工作内存中直接使用一个未被初始化load 或 assign 的变量。即就是对一个变量实施 use 和 store 操作之前必须先执行过了 load 或 assign 操作。 规则6一个变量在同一个时刻只允许一条线程对其进行 lock 操作但 lock 操作可以被同一条线程重复执行多次多次执行 lock 后只有执行相同次数的 unlock 操作变量才会被解锁。所以 lock 和 unlock 必须成对出现。 规则7如果对一个变量执行 lock 操作将会清空工作内存中此变量的值在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值。 规则8如果一个变量事先没有被 lock 操作锁定则不允许对它执行 unlock 操作也不允许去 unlock 一个被其他线程锁定的变量。 规则9对一个变量执行 unlock 操作之前必须先把此变量同步到主内存中执行 store 和 write 操作
看起来这些规则有些繁琐其实也不难理解 规则1、规则2 工作内存中的共享变量作为主内存的副本主内存变量的值同步到工作内存需要read和load一起使用工作内存中的变量的值同步回主内存需要store和write一起使用这2组操作各自都是是一个固定的有序搭配不允许单独出现。 规则3、规则4 由于工作内存中的共享变量是主内存的副本为保证数据一致性当工作内存中的变量被字节码引擎重新赋值必须同步回主内存。如果工作内存的变量没有被更新不允许无原因同步回主内存。 规则5 由于工作内存中的共享变量是主内存的副本必须从主内存诞生。 规则6、7、8、9 为了并发情况下安全使用变量线程可以基于lock操作独占主内存中的变量其他线程不允许使用或unlock该变量直到变量被线程unlock。 2、内存交互的3个特性
Java内存模型是围绕着在并发过程中如何处理这3个特性来建立的即原子性、有序性、可见性。它是如何保证这三种特性的呢
原子性、可见性、有序性
原子性 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断要么就都不执行。即使在多个线程一起执行的时候一个操作一旦开始就不会被其他线程所干扰。 JMM保证的原子性变量操作包括read、load、assign、use、store、write而long、double非原子协定导致的非原子性操作基本可以忽略。如果需要对更大范围的代码实行原子性操作则需要JMM提供的lock、unlock、synchronized等来保证。 可见性 是指当多个线程访问同一个变量时一个线程修改了这个变量的值其他线程能够立即看得到修改的值。 JMM在变量修改后将新值同步回主内存依赖主内存作为媒介在变量被线程读取前从内存刷新变量新值保证变量的可见性。普通变量和volatile变量都是如此只不过volatile的特殊规则保证了这种可见性是立即得知的而普通变量并不具备这种严格的可见性(不知道何时写到主存线程读取到可能为旧值)。除了volatile外synchronized和final也能保证可见性。 有序性 JMM的有序性规则表现在以下两种场景: 线程内和线程间 线程内 从某个线程的角度看方法的执行指令会按照一种叫“串行”as-if-serial的方式执行此种方式已经应用于顺序编程语言。线程间 这个线程“观察”到其他线程并发地执行非同步的代码时由于指令重排序优化任何代码都有可能交叉执行。唯一起作用的约束是对于同步方法同步块(synchronized关键字修饰)以及volatile字段的操作仍维持相对有序。 volatile的可见性、有序性
可见性 保证了不同线程对该变量操作的内存可见性。 当一个共享变量被volatile修饰时它会保证当前线程修改的值立即被更新到主存。 其他线程工作内存中的变量会强制立即失效当其他线程需要读取时会去主内存中读取最新值。 但是如果多个线程同时把更新后的变量值同时刷新回主内存可能导致得到的值不是预期结果 举个例子定义volatile int count 02个线程同时执行count操作每个线程都执行500次最终结果小于1000原因是每个线程执行count需要以下3个步骤 步骤1 线程从主内存读取最新的count的值步骤2 执行引擎把count值加1并赋值给线程工作内存步骤3 线程工作内存把count值保存到主内存 有可能某一时刻2个线程在步骤1读取到的值都是100执行完步骤2得到的值都是101最后刷新了2次101保存到主内存 有序性 原理volatile的可见性和有序性都是通过加入内存屏障来实现。 会在写之后加入一条store屏障指令将本地内存中值刷新到主内存。 会在读之前加入一条load屏障指令从主内存中读取共享变量。
具体一点 当程序执行到 volatile变量的读操作或者写操作时在其前面的操作的更改肯定全部已经进行且结果已经对后面的操作可见在其后面的操作肯定还没有进行 在进行指令优化时不能将在对 volatile 变量访问的语句放在其后面执行也不能把 volatile 变量后面的语句放到其前面执行。 synchronized的原子性、可见性、有序性
synchronized能够在同一时刻最多只有一个线程执行该代码已达到并发线程同步安全的效果
synchronized能保证线程的原子性可见性和有序性
synchronized能保证线程的原子性原理synchronized保证只有一个线程拿到锁即在同一时刻只有一个线程执行同步代码块或同步方法。
synchronized能保证可见性的原理执行synchronized的方法对应的是lock原子操作会刷新线程工作内存中的共享变量从而使其得到最新值。
synchronized能保证有序性原理加入synchronized后依然会发生重排序只不过同步代码块可以保证只有一个线程执行同步代码块中代码从而保证有序性。
3、重排序
在执行程序时为了提高性能编译器和处理器经常会对指令进行重排序。从硬件架构上来说指令重排序是指CPU采用了允许将多条指令不按照程序规定的顺序分开发送给各个相应电路单元处理而不是指令任意重排。重排序分成三种类型 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下可以重新安排语句的执行顺序。指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性处理器可以改变语句对应机器指令的执行顺序。内存系统的重排序。由于处理器使用缓存和读写缓冲区这使得加载和存储操作看上去可能是在乱序执行。 4、JMM的重排序屏障
从Java源代码到最终实际执行的指令序列会经过三种重排序。但是为了保证内存的可见性Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。对于编译器的重排序JMM会根据重排序规则禁止特定类型的编译器重排序对于处理器重排序JMM会插入特定类型的内存屏障通过内存的屏障指令禁止特定类型的处理器重排序。这里讨论JMM对处理器的重排序为了更深理解JMM对处理器重排序的处理先来认识一下常见处理器的重排序规则 其中的N标识处理器不允许两个操作进行重排序Y表示允许。其中Load-Load表示读-读操作、Load-Store表示读-写操作、Store-Store表示写-写操作、Store-Load表示写-读操作。可以看出常见处理器对写-读操作都是允许重排序的并且常见的处理器都不允许对存在数据依赖的操作进行重排序对应上面数据转换那一列都是N所以处理器不允许这种重排序。
那么这个结论对我们有什么作用呢比如第一点处理器允许写-读操作两者之间的重排序那么在并发编程中读线程读到可能是一个未被初始化或者是一个NULL等出现不可预知的错误基于这点JMM会在适当的位置插入内存屏障指令来禁止特定类型的处理器的重排序。内存屏障指令一共有4类
LoadLoad屏障 对于这样的语句 Load1; LoadLoad; Load2在Load2及后续读取操作要读取的数据被访问前保证Load1要读取的数据被读取完毕。
StoreStore屏障 对于这样的语句 Store1; StoreStore; Store2在Store2及后续写入操作执行前保证Store1的写入操作对其它处理器可见。
LoadStore屏障 对于这样的语句Load1; LoadStore; Store2在Store2及后续写入操作被执行前保证Load1要读取的数据被读取完毕。
StoreLoad屏障 对于这样的语句Store1; StoreLoad; Load2在Load2及后续所有读取操作执行前保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的冲刷写缓冲器清空无效化队列。在大多数处理器的实现中这个屏障是个万能屏障兼具其它三种内存屏障的功能。
数据依赖性
根据上面的表格处理器不会对存在数据依赖的操作进行重排序。这里数据依赖的准确定义是如果两个操作同时访问一个变量其中一个操作是写操作此时这两个操作就构成了数据依赖。常见的具有这个特性的如i、i—。如果改变了具有数据依赖的两个操作的执行顺序那么最后的执行结果就会被改变。这也是不能进行重排序的原因。例如
写后读a 1; b a; 写后写a 1; a 2; 读后写a b; b 1; 重排序遵守数据依赖性编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。但是这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
as-if-serial语义
as-if-serial语义的意思指管怎么重排序编译器和处理器为了提高并行度单线程程序的执行结果不能被改变。编译器runtime 和处理器都必须遵守as-if-serial语义。
as-if-serial语义把单线程程序保护了起来遵守as-if-serial语义的编译器runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会干扰他们也无需担心内存可见性问题。
重排序对多线程的影响
如果代码中存在控制依赖的时候会影响指令序列执行的并行度因为高效。也是为此编译器和处理器会采用猜测Speculation执行来克服控制的相关性。所以重排序破坏了程序顺序规则该规则是说指令执行顺序与实际代码的执行顺序是一致的但是处理器和编译器会进行重排序只要最后的结果不会改变该重排序就是合理的。
在单线程程序中由于as-ifserial语义的存在对存在控制依赖的操作重排序不会改变执行结果但在多线程程序中对存在控制依赖的操作重排序可能会改变程序的执行结果。
5、happens-before 先行发生原则
happens-before关系用于描述下2个操作的内存可见性如果操作A happens-before 操作B那么A的结果对B可见 happens-before关系的分析需要分为单线程和多线程的情况 单线程下的 happens-before 字节码的先后顺序天然包含happens-before关系因为单线程内共享一份工作内存不存在数据一致性的问题。 在程序控制流路径中靠前的字节码 happens-before 靠后的字节码即靠前的字节码执行完之后操作结果对靠后的字节码可见。然而这并不意味着前者一定在后者之前执行。实际上如果后者不依赖前者的运行结果那么它们可能会被重排序。 多线程下的 happens-before 多线程由于每个线程有共享变量的副本如果没有对共享变量做同步处理线程1更新执行操作A共享变量的值之后线程2开始执行操作B此时操作A产生的结果对操作B不一定可见。
倘若在开发中仅靠synchronized和volatile来保证顺序性、原子性、可见性。那么编写并发程序会十分麻烦。在Java内存模型中还提供了happens-before原则来辅助保证程序执行的原子性、可见性、有序性的问题。它是判断数据是否存在竞争线程是否安全的依据。 Java内存模型实现了下述支持happens-before关系的操作 程序次序规则 一个线程内按照代码顺序书写在前面的操作 happens-before 书写在后面的操作。 锁定规则 一个unLock操作 happens-before 后面对同一个锁的lock操作。 volatile变量规则 对一个volatile变量的写操作 happens-before 后面对这个变量的读操作。 传递规则 如果操作A happens-before 操作B而操作B又 happens-before 操作C则可以得出操作A happens-before 操作C。 线程启动规则 Thread对象的start()方法 happens-before 此线程的每个一个动作。 线程中断规则 对线程interrupt()方法的调用 happens-before 被中断线程的代码检测到中断事件的发生。 线程终结规则 线程中所有的操作都 happens-before 线程的终止检测我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。 对象终结规则 一个对象的初始化完成 happens-before 他的finalize()方法的开始 注意不同操作时间先后顺序与先行发生原则之间没有关系二者不能相互推断衡量并发安全问题不能受到时间顺序的干扰一切都要以happens-before原则为准
示例代码1:
private int value 0;public void setValue(int value) {this.value value;
}public int getValue() {return this.value;
}对于上面的代码假设线程A在时间上先调用setValue(1)然后线程B调用getValue()方法那么线程B收到的返回值一定是1吗
按照happens-before原则两个操作不在同一个线程、没有通道锁同步、线程的相关启动、终止和中断以及对象终结和传递性等规则都与此处没有关系因此这两个操作是不符合happens-before原则的这里的并发操作是不安全的返回值并不一定是1。
对于该问题的修复可以使用lock或者synchronized套用“管程锁定规则”实现先行发生关系或者将value定义为volatile变量两个方法的调用都不存在数据依赖性套用“volatile变量规则”实现先行发生关系。如此一来就能保证并发安全性。
示例代码2
// 以下操作在同一个线程中
int i 1;
int j 2;上面的代码符合“程序次序规则”满足先行发生关系但是第2条语句完全可能由于重排序而被处理器先执行时间上先于第1条语句。
final变量的特殊规则
我们知道final成员变量必须在声明的时候初始化或者在构造器中初始化否则就会报编译错误。 final关键字的可见性是指被final修饰的字段在声明时或者构造器中一旦初始化完成那么在其他线程无须同步就能正确看见final字段的值。这是因为一旦初始化完成final变量的值立刻回写到主内存。
作者浪里小白龙 链接https://www.jianshu.com/p/d87e9b6747b0 来源简书 著作权归作者所有。商业转载请联系作者获得授权非商业转载请注明出处。
long 、doube类型的变量的特殊规则 Java内存模型要求lock、unlock、read、load、assign、use、store、write这8种操作都具有原子性但是对于64位的数据类型(long和double)在模型中特别定义相对宽松的规定允许虚拟机将没有被volatile修饰的64位数据的读写操作分为2次32位的操作来进行。也就是说虚拟机可选择不保证64位数据类型的load、store、read和write这4个操作的原子性。由于这种非原子性有可能导致其他线程读到同步未完成的“32位的半个变量”的值。 不过实际开发中Java内存模型强烈建议虚拟机把64位数据的读写实现为具有原子性目前各种平台下的商用虚拟机都选择把64位数据的读写操作作为原子操作来对待因此我们在编写代码时一般不需要把用到的long和double变量专门声明为volatile。 三、JVM运行时数据区Java内存区域
看这篇 参考文章 3. 深入理解JVM: Java内存模型JMM 4. JVM七JMM内存模型 5. Java内存区域(运行时数据区域) 和 内存模型(JMM) 6. JVM内存模型 7. Java内存模型