title: Java 内存模型_1
date: 2017-01-15 17:11:02
tags: [JMM]
categories: [Programming,Java]
---
本文记录 Java 中的内存模型的基础部分1。
本篇作为学习 Java 内存模型基础部分的笔记,加上些许自己的理解和解释.
结论:并发产生的内存可见性问题.
并发编程中的两个关键问题:
已有的通信机制:
同步,指的是控制不同线程之间操作发生的相对顺序的机制:
java 采用的是共享内存模型,而 java 线程之间的通信总是隐式进行,整个通信过程对程序员完全是透明的.
对于 java 程序员而言,如果不了解 java 内存模型,在编写多线程程序的时候,就会遇到各种各样的内存可见性的问题.所以,对 java 的内存模型需要有一定的了解.
Java 采用的是共享内存模型作为线程间的通信机制.
共享内存: 堆内存,在 java 中,所有的 实例域,静态域和数组元素 存储在堆内存中,堆内存在线程之间共享.
局部变量,方法定义的参数和异常处理参数,不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响.
Java 线程之间的通信由 Java内存模型(JMM)控制,JMM 决定了一个线程对共享变量的写入何时对另个线程可见.从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读写共享变量的副本.本地内存是 JMM 的一个抽象概念,并不真实存在.它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化.
分析: 线程 A 和线程 B 通信过程
本地内存 A 和本地内存 B 都有主内存中共享的变量 x 的副本.假设初始时,这个三个内存中的 x 的值都是 0.线程 A 在执行时,把已经更新的 x 值(假设为1) 临时存放在自己的本地内存 A 中. 当线程 A 和线程 B 需要通信的时,线程 A 首先会把本地内存中修改后的 x 值刷新到主内存中,此时主内存中的 x 值变为 1.随后,线程 B 到主内存中读取线程 A 更新之后的 x 值,此时线程 B 的本地内存的 x 值也变为 1.
从整体上看,这两个步骤实质上是线程 A 在向线程 B 发送消息,而且这个通信过程必须经过主内存.JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 java 程序员提供内存可见性保证.
在执行程序的时候,为了提高性能,编译器 和 处理器 常常会对指令做重排序.有三种重排序.
三个重排序,都可能导致多线程出现内存可见性的问题.
假设处理器 A 和处理器 B 按程序的顺序并行执行内存访问,最终却可能得到 x = y = 0 的结果。具体的原因如下图所示:
从内存操作实际发生的顺序来看,直到处理器 A 执行 A3 来刷新自己的写缓存区,写操作 A1 才算真正执行了。虽然处理器 A 执行内存操作的顺序为:A1->A2,但内存操作实际发生的顺序却是:A2->A1。此时,处理器 A 的内存操作顺序被重排序了(处理器 B 的情况和处理器 A 一样,这里就不赘述了)。
这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作重排序。
对于编译器, JMM 的编译器重排序规则会禁止特定类型的编译器重排序(并不是所有的编译器重排序都需要被禁止的).
对于处理器, JMM 的处理器重排序规则会要求 java 编译器在生成指令序列的时候,插入指定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止的).
现代处理器使用写缓冲区来临时保存向内存写入的数据.写缓冲区可以保障指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生延迟.同时通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一个内存地址的多次写,可以减少对内存总线的占用.
缓冲区的这一特性是可以加速程序的运行,然而每个处理器的写缓冲区,仅仅对它所在的处理器可见.这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读写操作的执行顺序,不一定与内存实际发生的读写顺序一致.
上表单元格中的 “N” 表示处理器不允许两个操作重排序,“Y” 表示允许重排序。
从上表我们可以看出:常见的处理器都允许 Store-Load 重排序;常见的处理器都不允许对存在数据依赖的操作做重排序。sparc-TSO 和 x86 拥有相对较强的处理器内存模型,它们仅允许对写-读操作做重排序(因为它们都使用了写缓冲区)。
为了保证内存可见性,java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障指令分为下列四类:
StoreLoad Barriers 是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)。
从 JDK5 开始,java 使用新的 JSR -133 内存模型,JSR-133 使用 happens-before 的概念来阐述操作之间的内存可见性。在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
与程序员密切相关的 happens-before 规则如下:
注意,两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second).
happens-before 与 JMM 的关系.
如上图所示,一个 happens-before 规则通常对应于多个编译器和处理器重排序规则。对于 java 程序员来说,happens-before 规则简单易懂,它避免 java 程序员为了理解 JMM 提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现。
感谢以下的文章以及其作者和翻译的开发者们,排名不分先后
原文:https://www.cnblogs.com/jnienv/p/10575623.html