深入理解java内存模型程晓明_深入理解Java内存模型(技术干货)

深⼊理解java内存模型程晓明_深⼊理解Java内存模型(技术
⼲货)
前提
《深⼊理解 Java 内存模型》程晓明著,该书在以前看过⼀遍,现在学的东西越多,感觉那块越重要,于是⼜再细看⼀遍,于是便有了下⾯的读书笔记总结。全书页数虽不多,内容讲得挺深的。细看的话,也是挺花时间的,看完收获绝对挺⼤的。也建议 Java 开发者都去看看。⾥⾯主要有 Java 内存模型的基础、重排序、顺序⼀致性、Volatile 关键字、锁、final。本⽂参考书中内容。
男人体照片基础
并发编程的模型分类
在并发编程需要处理的两个关键问题是: 线程之间如何通信 和 线程之间如何同步 。
通信
通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种: 共享内存 和 消息传递 。
在 共享内存 的并发模型⾥,线程之间共享程序的公共状态,线程之间通过写-读内存中的 公共状态 来 隐式 进⾏通信。
在 消息传递 的并发模型⾥,线程之间没有公共状态,线程之间必须通过明确的 发送消息 来 显式 进⾏通信。
同步
同步是指程序⽤于控制不同线程之间操作发⽣相对顺序的机制。
在 共享内存 的并发模型⾥,同步是 显式 进⾏的。程序员必须显式指定某个⽅法或某段代码需要在线程之间 互斥执⾏ 。
在 消息传递 的并发模型⾥,由于消息的发送必须在消息的接收之前,因此同步是 隐式 进⾏的。
Java 的并发采⽤的是共享内存模型,Java 线程之间的通信总是隐式进⾏,整个通信过程对程序员完全透明。
Java 内存模型的抽象
在 Java 中,所有实例域、静态域 和 数组元素存储在堆内存中,堆内存在线程之间共享。局部变量、⽅法定义参数 和 异常处理器参数 不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
Java 线程之间的通信由 Java 内存模型(JMM)控制。JMM 决定了⼀个线程对共享变量的写⼊何时对另⼀个线程可见。从抽象的⾓度来看,JMM 定义了线程与主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每⼀个线程都有⼀个⾃⼰私有的本地内存,本地内存中存储了该变量以读/写共享变量的副本。本地内存是 JMM 的⼀个抽象概念,并不真实存在。
JMM 抽象⽰意图:
从上图来看,如果线程 A 和线程 B 要通信的话,要如下两个步骤:
1、线程 A 需要将本地内存 A 中的共享变量副本刷新到主内存去
2、线程 B 去主内存读取线程 A 之前已更新过的共享变量
步骤⽰意图:
举个例⼦:
本地内存 A 和 B 有主内存共享变量 X 的副本。假设⼀开始时,这三个内存中 X 的值都是 0。线程 A 正执⾏时,把更新后的 X 值(假设为1)临时存放在⾃⼰的本地内存 A 中。当线程 A 和 B 需要通信时,线程 A ⾸先会把⾃⼰本地内存 A 中修改后的 X 值刷新到主内存去,此时主内存中的 X 值变为了 1。随后,线程 B 到主内存中读取线程 A 更新后的共享变量 X 的值,此时线程 B 的本地内存的 X 值也变成了 1。
整体来看,这两个步骤实质上是线程 A 再向线程 B 发送消息,⽽这个通信过程必须经过主内存。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 Java 程序员提供内存可见性保证。
重排序
在执⾏程序时为了提⾼性能,编译器和处理器常常会对指令做重排序。重排序分三类:
1、 编译器优化的重排序 。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执⾏顺序。
2、 指令级并⾏的重排序 。现代处理器采⽤了指令级并⾏技术来将多条指令重叠执⾏。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执⾏顺序。
3、 内存系统的重排序 。由于处理器使⽤缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执⾏。
从 Java 源代码到最终实际执⾏的指令序列,会分别经历下⾯三种重排序:
上⾯的这些重排序都可能导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁⽌特定类型的编译器重排序(不是所有的编译器重排序都要禁⽌)。对于处理器重排序,JMM 的处理器重排序规则会要求 Java 编译器在⽣成指令序列时,插⼊特定类型的内存屏障指令,通过内存屏障指令来禁⽌特定类型的处理器重排序(不是所有的处理器重排序都要禁⽌)。
JMM 属于语⾔级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁⽌特定类型的编译器重排序和处理器重排序,为程序员提供⼀致的内存可见性保证。
处理器重排序
现代的处理器使⽤ 写缓冲区 来临时保存向内存写⼊的数据。写缓冲区可以保证指令流⽔线持续运⾏,
它可以避免由于处理器停顿下来等待向内存写⼊数据⽽产⽣的延迟。同时,通过以批处理的⽅式刷新写缓冲区,以及合并写缓冲区中对同⼀内存地址的多次写,可以减少对内存总线的占⽤。虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执⾏顺序产⽣重要的影响:处理器对内存的读/写操作的执⾏顺序,不⼀定与内存实际发⽣的读/写操作顺序⼀致!
举个例⼦:
假设处理器A和处理器B按程序的顺序并⾏执⾏内存访问,最终却可能得到 x = y = 0。具体的原因如下图所⽰:
exam1-ans
经济危机论文
处理器 A 和 B 同时把共享变量写⼊在写缓冲区中(A1、B1),然后再从内存中读取另⼀个共享变量(A2、B2),最后才把⾃⼰写缓冲区中保存的脏数据刷新到内存中(A3、B3)。当以这种时序执⾏时,程序就可以得到 x = y = 0 的结果。
从内存操作实际发⽣的顺序来看,直到处理器 A 执⾏ A3 来刷新⾃⼰的写缓存区,写操作 A1 才算真正执⾏了。虽然处理器 A 执⾏内存操作的顺序为:A1 -> A2,但内存操作实际发⽣的顺序却是:A2 -> A1。此时,处理器 A 的内存操作顺序被重排序了。
这⾥的关键是,由于 写缓冲区仅对⾃⼰的处理器可见,它会导致处理器执⾏内存操作的顺序可能会与内存实际的操作执⾏顺序不⼀致 。由于现代的处理器都会使⽤写缓冲区,因此现代的处理器都会允许对写-读操作重排序。
内存屏障指令
为了保证内存可见性,Java 编译器在⽣成指令序列的适当位置会插⼊ 内存屏障指令 来禁⽌特定类型的处理器重排序。JMM 把内存屏障指令分为下列四类:
happens-before
JSR-133 内存模型使⽤ happens-before 的概念来阐述操作之间的内存可见性。在 JMM 中,如果 ⼀个操作执⾏的结果需要对另⼀个操作可见 ,那么这两个操作之间必须要存在 happens-before 关系。这⾥提到的两个操作既可以是在⼀个线程之内,也可以是在不同线程之间。
与程序员密切相关的 happens-before 规则如下:
程序顺序规则:⼀个线程中的每个操作,happens-before 于该线程中的任意后续操作。
监视器锁规则:对⼀个监视器的解锁,happens-before 于随后对这个监视器的加锁。
volatile 变量规则:对⼀个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
注意,两个操作之间具有 happens-before 关系,并不意味着前⼀个操作必须要在后⼀个操作之前执⾏!happens-before 仅仅要求前⼀个操作(执⾏的结果)对后⼀个操作可见,且前⼀个操作按顺序排在第⼆个操作之前(the first is visible to and ordered before the second)。
happens-before 与 JMM 的关系如下图所⽰:
如上图所⽰,⼀个 happens-before 规则对应于⼀个或多个编译器和处理器重排序规则。氯仿
数据依赖性
如果两个操作访问同⼀个变量,且这两个操作中有⼀个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型:
名称代码⽰例说明写后读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 编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执⾏结果。但是如果操作之间没有数据依赖关系,这些操作就可能被编译器和处理器重排序。
举个例⼦:
上⾯三个操作的数据依赖关系如下图所⽰:
如上图所⽰,A 和 C 之间存在数据依赖关系,同时 B 和 C 之间也存在数据依赖关系。因此在最终执⾏的指令序列中,C 不能被重排序到 A 和 B 的前⾯(C 排到 A 和 B 的前⾯,程序的结果将会被改变)。
但 A 和 B 之间没有数据依赖关系,编译器和处理器可以重排序 A 和 B 之间的执⾏顺序。下图是该程序的两种执⾏顺序:
在计算机中,软件技术和硬件技术有⼀个共同的⽬标:在不改变程序执⾏结果的前提下,尽可能的开发并⾏度。编译器和处理器遵从这⼀⽬标,从 happens-before 的定义我们可以看出,JMM 同样遵从这⼀⽬标。
重排序对多线程的影响
举例:
由于操作 1 和 2 没有数据依赖关系,编译器和处理器可以对这两个操作重排序;操作 3 和操作 4 没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。
质量跟踪
1、当操作 1 和操作 2 重排序时,可能会产⽣什么效果?

本文发布于:2024-09-21 23:30:44,感谢您对本站的认可!

本文链接:https://www.17tex.com/xueshu/155042.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:内存   处理器   操作   排序   线程   编译器   数据
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议