一.Java内存模型的抽象结构
- 局部变量、方法定义参数、异常处理器参数不会在线程间共享,不存在内存可见性问题,不受内存模型影响。
- 实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。
- Java线程之间的通信由Java内存模型(JMM)控制。线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存存储了该线程读\写共享变量的副本。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性的保证。
二.重排序
1.简介
- 为了提高性能,指令可能会被重排序,重排序分三种:
(1)编译器优化重排序:编译器在不改变单线程程序语义的前提下,可重新安排语句的执行顺序。
(2)指令级并行重排序:如果不存在数据依赖,处理器可改变语句对应机器指令的执行顺序。
(3)内存系统重排序:处理器在读\写缓冲器时,看起来像是在乱序执行。 - 从Java源代码到最终执行的指令序列,会分别经历这3种重排序。(1)属于编译器重排序,JMM会禁止特定类型的编译器重排序。(2)(3)是处理器重排序,JMM使用内存屏障来禁止某些处理器重排序。
2.内存屏障
JMM把内存屏障分为4类
从图中看出,StoreLoad屏障是全能型屏障,它同时具有其他3种屏障的功能。
3.其他概念
- 数据依赖性:如果两个操作访问同一个变量,且其中一个操作为写操作,则这两个操作存在数据依赖性。
- as-if-serial语义:即不管怎么重排序,单线程程序的执行结果不能被改变。为了遵循as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作进行重排序。
三.顺序一致性
1.顺序一致性的特征
- 一个线程中的所有操作必须按照程序的顺序来执行。
- 不管程序是否同步,所有线程都只能看到一个单一的操作执行顺序,每个操作都是原子执行并且立刻对所有线程可见。
2.JMM与顺序一致性
- JMM中线程可能发生指令重排序,所以顺序一致性的第一个特征不满足。
- 如果多个线程未同步,所以线程看到的操作执行顺序也可能不一致。(例如:A线程把共享变量写入本地内存,在这个共享变量被刷新到主内存之前,这个写操作只对A可见。但对其他线程来说,A并未执行这个写操作。)所以顺序一致性的第二个特征不满足。
总结就是:如果多个线程正确同步,那JMM保证这些线程的执行结果与它们在顺序一致性模型中的执行结果一致。否则,JMM什么都不保证。
3.零碎知识点
从JDK5开始64位long/double类型变量的写操作拆分为两个32位写,但读操作都是原子的。
四.happens-before
1.介绍
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作就存在happens-before关系。
2.JMM的设计
(1) int a=1;
(2) int b=3;
(3) int c=a+b;
上述代码存在3个happens-before关系(简称bf)
A . (1)bf(2)
B. (2)bf(3)
C. (1)bf(3)
A是不必须的,但BC必须。因此,JMM把bf要求静止的重排序分为:(I)会改变程序执行结果的重排序和(II)不会改变程序执行结果的重排序。
JMM要求编译器和处理器禁止(I)重排序,对(II)不作要求。
3.happens-before八大原则
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。即无论在单线程还是多线程中,同一个锁如果处于被锁定状态,那么必须先对锁进行释放操作,后面才能继续执行lock操作。
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
- 传递规则:如果操作A先行发生于操作B,而操作B先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动原则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
- 线程终结规则:线程中所以的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thhread.isAlive()的返回值手段检测到线程已经终止执行。
- 对象终结规则:一个对象的初始化完成先行于它的finalize(()方法的开始。
4.总结
- happens-before 和 as-if-serial 目标是一样的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
- JMM并不保证时时刻刻都满足happens-before,JMM只保证如果线程间正确同步了,那么多线程的执行能够达到happens-before 和 as-if-serial 期望的目标。