内存模型的基础
- 通信 线程之间以何种机制来交换信息
- 共享内存 隐式通信
- 消息传递 显示通信
- 同步 程序中用于控制不同线程间操作,发生的相对顺序的机制
- 共享内存 显式同步
- 消息传递 隐式同步
Java线程线程之间是通过共享内存的方式实现通信的.
内存模型的抽象结构
- 共享变量
共享变量手内存模型影响,线程会去主内存里去加载共享变量,当线程需要改变共享变量时,会将本地内存已更改的副本提交到主内存.
- 局部变量
局部变量不会受内存模型的影响
线程之间通信
指令重排
- 编译器优化的重排序
- 指令级并行的重排序
- 内存系统的重排序
什么是指令重排?
int i=0; 2 int j=1;
按照我们的认知,程序是一行一行往下执行的,但是由于编译器或运行时环境为了优化程序性能,采取对指令进行重新排序执行,也就是说在计算机执行上面两句话的时候,有可能第二条语句会优先于第一条语句执行.
然而并不是所有的指令都能重排,重排需要基于数据依赖性.
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型:
名称 | 代码示例 | 说明 |
---|---|---|
写后读 | a=1;b=a; | 写一个变量之后,再读这个位置. |
写后写 | a=1;a=2; | 写一个变量之后,再写这个变量. |
读后写 | a=b;b=1; | 读一个变量之后,再写这个变量. |
上面的情况,如果重排序了两个操作的执行顺序,程序的执行结果将会跟预期完全不一样.
所以说,虽然编译器和处理器可能会对操作做重排序,但是编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
as-if-serial
定义:不管怎么重排序(编译器和处理器为了提⾼并⾏度),(单线程) 程序的执⾏结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
happens-before
happens-before是JMM的最核心概念之一
JMM设计意图
- 程序员对内存模型的使用
- 为程序员提供足够强的内存可见性保证
- 编译器和处理器对内存模型的实现
- 对编译器和处理器的限制要尽可能的放松
JMM禁止:
禁止编译器和处理器会改变程序执行结果的重排序.
JMM允许:
允许编译器和处理器不会改变程序执行结果的重排序.
happens-before规则
在JMM中,如果⼀个操作执⾏的结果需要对另⼀个操作可⻅,那么这两个操作之间必须要存在happens-before关系.
- 程序顺序规则 ⼀个线程中的每个操作,happens-before于该线程中的任意后续操作.
- 监视器锁规则 对⼀个锁的解锁,happens-before于随后对这个锁的加锁.
- volatile变量规则 对⼀个volatile域的写,happens-before于任意后续对这个volatile域的读.
- 传递性 如果A happens-before B,且B happens-before C,那么A happens-before C.
- start()规则 如果线程A执⾏操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- join()规则 如果线程A执⾏操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()返回
- 线程中断规则 对线程interrupt⽅法的调⽤happens-before于被中断线程的代码检测到中断事件的发⽣.
- 对象终结规则 ⼀个对象的初始化的完成,也就是构造函数执⾏的结束⼀定 happens-before它的finalize()⽅法.
JMM向程序员提供的happens-before规则能满⾜程序员的需求.
JMM对编译器和处理器的束缚已经尽可能少.
JMM对程序员的承诺
如果⼀个操作happens-before另⼀个操作,那么第⼀个操作的执⾏结果将对第⼆个操作 可⻅,⽽且第⼀个操作的执⾏顺序排在第⼆个操作之前.
JMM对编译器和处理器重排序的约束原则
两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执⾏.
例子:
1 public class Demo29 {
2 int a=0;
3 boolean flag=false;
4 public void writer(){