Java内存模型的基础
1. 并发编程模型的两个关键问题
两个问题:
- 线程之间如何通信
- 线程之间如何同步
通信:线程之间以何种机制来交换信息
同步:程序组用于控制不同线程间操作发生相对顺序的机制
两种并发模型:
- 共享内存:线程之间共享程序的公共状态,通过写-读内存中的公共状态来隐式通信,同步是显式进行的
- 消息传递:线程之间没有公共状态,线程之间必须通过发送消息来显式通信,同步是隐式进行的
Java的并发采用共享内存模型
2. Java内存模型的抽象结构
Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。
局部变量、方法定义参数和异常处理器参数不会在线程之间共享,因此没有内存可见性问题,不受内存模型影响。
线程之间的通信由Java内存模型(即JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
从抽象的角度来说,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每一个线程都有对应的本地内存,本地内存中存储了该线程读/写共享变量的副本。
本地内存只是JMM的一个抽象概念,其并不存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
JMM是一种语言级的内存模型,它通过控制主内存与每个线程的本地内存之间的交互,来为程序员提供内存可见性保证。
3. 从源代码到指令序列的重排序
重排序有以下三种类型:
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2)指令级并行的重排序。指令级并行技术可以将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3)内存系统的重排序。处理器使用缓存和读 / 写缓冲区,这导致加载和存储操作看上去可能是乱序执行。
从Java源代码到最终实际执行的指令序列,会分别经历这三种重排序
其中,1属于编译器重排序,2、3属于处理器重排序
重排序的后果:可能会导致多线程程序出现内存可见性问题
对于编译器,JMM会禁止特定类型的编译器重排序,对于处理器,JMM会要求Java编译器在生成指令序列时插入特定类型的内存屏障来禁止特定类型的处理器重排序
4. 并发编程模型的分类
现代处理器使用写缓冲区临时保存向内存写入的数据
优点:
- 写缓冲区可以保证指令流水线持续运行,避免由于处理器停顿等待向内存写入数据而产生的延迟
- 通过批处理的方式刷新写缓存区和合并写缓存区中对同一内存地址的多次写,减少对内存总线的占用
缺点:每个处理器上的写缓冲区仅对其处理器可见,该特性会导致处理器对内存的读 / 写操作顺序与内存实际发生的读 / 写操作顺序不一定一致
由于该缺点,现代处理器都允许对写 - 读操作重排序
处理器的重排序规则
处理器\规则 | Load-Load | Load-Store | Store-Store | Store-Load | 数据依赖 |
---|---|---|---|---|---|
SPARC-TSO | N | N | N | Y | N |
x86 | N | N | N | Y | N |
IA64 | Y | Y | Y | Y | N |
PowerPC | Y | Y | Y | Y | N |
常见的处理器都允许Store-Load重排序,都不允许对存在数据依赖的操作做重排序
Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序
内存屏障类型表
屏障类型 | 指令示例 | 说明 |
---|---|---|
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; Load2 | 确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2及之后所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才会执行该屏障之后的内存访问指令。 |
StoreLoad Barriers是一个”全能型“屏障,它同时具有其他三个屏障的功能,但是该屏障的开销很昂贵,因为处理器通常要把写缓冲区的数据全部刷新到内存中
5. happens-before简介
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这两个操作可以在一个线程内,也可以是在不同线程之间。
与程序员密切相关的happens-before规则:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
- 监视器锁规则:一个锁的解锁happens-before于随后这个锁的加锁
- volatile变量规则:对一个volatile域的写happens-before于任意后续对该域的读
- 传递性规则:如果A happens-before B,B happens-before C,那么A happens-before C
happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。
happens-before与JMM的关系
一个happens-before规则对应于多个编译器或处理器重排序规则