文章目录
一、JMM概述
1. Java内存模型的抽象结构
Java线程之间的通信由Java内存模型(简称JMM)控制,JMM决定了一个线程对共享变量的写入何时对另一个线程可见,从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每一个线程都有一个私有的本地内存,本地内存存储了该线程可以读 / 写的共享变量的副本。本地内存是JMM的一个抽象概念,并不是真实存在的。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
JMM体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
如图:(画图确实有点丑)
所以如果A和B线程之间要通信的话,首先线程A需要把本地内存中更新过的共享变量刷新到主内存中,然后线程B从主内存中重新读取修改过的共享变量
JMM通过控制主内存与线程本地内存之间的交互来保证Java的内存可见性
二、重排序
在执行程序时,为了提高性能,编译器中和处理器常常会对指令做重排序。重排序分3种类型。
- 编译器优化的重排序编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
数据依赖性
例如
a = 1;
b = a;
a = 1;
a = 2;
编译器和处理器在重排序时,会遵循数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序
as-if-serial 语义
例如
int a = 1; // A
int b = 2; // B
int c = a + b;// C
as-if-serial 语义的意思是:不管怎么重排序,(单线程)程序的执行结果不能被改变。该程序C依赖于A、B所以C只会在最后执行,不会被重排序。但是AB谁先对结果没有影响,所以可能会被重排序。
三、volatile 的内存语义
1. volatile 的特性
- 可见性:对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入
- 原子性:对任意单个volatile变量的读写具有原子性,但类似于volatile++这种复合操作不具有原子性
2. volatile 内存语义
- 线程A写入一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所作的修改)消息。(当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存)
- 线程B读一个volatile变量,实质上是线程B接受了之前某个线程发出的(在写这个变量之前对共享变量所作的修改)消息。(当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主存中读取共享变量)
- 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息
1. volatile 内存语义的实现
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
- 在每个volatile写操作的前面插入一个StoreStore屏障:禁止上面的普通写和该volatile写重排序,并且将之前的普通写刷新到主内存
- 在每个volatile写操作后面插入一个StoreLoad屏障:防止该volatile写与下面可能有的volatile读/写重排序
- 在每个volatile读操作之后插入一个LoadLoad屏障:禁止之后所有的普通读操作和该volatile读重排序
- 在每个volatile读操作之后插入一个LoadStore屏障:禁止之后的所有普通写操作和该volatile读操作重排序
简单理解就是:写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后,读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
原理
java 代码 :
instance = new Singleton(); // instance 是 volatile 变量
转变成汇编代码:
0x01a3de: move $0 x 0, 0 x 1104800(%esi);
0x01a3de24: lock add1 $0 x 0,(%esp)
有volatile修饰的共享变量进行写操作的时候会多出第二行代码,主要在于Lock前缀
Lock 前缀的指令在多核处理器会引发两件事
- Lock 前缀指令会引起处理器缓存回写到内存
- 一个处理器的缓存回写到内存会导致其他处理器的缓存无效
四、final域的内存语义
1. final 域的重排序规则
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作不能重排序
举例:
/**
* @Description final域的重排序规则
* @Date 2021/6/7 22:35
* @Created by: A.iguodala
*/
public class FinalExample {
/**
* 普通变量
*/
int i;
/**
* final变量
*/
final int j;
/**
* 引用变量
*/
static FinalExample obj;
/**
* 构造函数
*/
public FinalExample() {
this.i = 1;
this.j = 2;
}
/**
* 代表第一条规则
* 写final域操作
*/
public static void writer() {
obj = new FinalExample();
}
/**
* 代表第二条规则
* 对于final域的读操作
*/
public static void reader() {
FinalExample object = obj;
int a = object.i;
int b = object.j;
}
}
第一条,写final域的重排序规则
- JMM禁止编译器把final域的写重排序到构造函数之外,编译器会在final域的写之后,插入一个StoreStore屏障
以如上代码为例,线程A执行writer方法,对于普通域 i 的初始化可能被重排序到构造函数之外,另一个线程可能会读到未被初始化的 i ,即读到0,而final域则一定在构造函数中被初始化
第二条,读final域的重排序规则
- 在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意:仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障
以如上代码为例,线程B可能在读方法中,在读对象obj引用之前读取普通域 i ,这样可能是还未被初始化的
存疑:在读对象引用之前如何能读到对象的域?没有存在数据依赖性吗?为什么会指令重排?
求大佬解答
2. final 域为引用类型
对于引用类型,写final 域的重排序规则对编译器和处理器增加了如下约束:构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造函数对象的引用赋值给一个引用变量,这两个操作之间不能重排序
五、happens-before
1. happens-before介绍
《JSP-133:Java Memory Model and Thread Specification》对happens - before 关系的定义如下
- 如果一个操作happens - before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前(这是JMM对程序员做出的保证)
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果是一致的,那么这种重排序并不非法(这是JMM对编译器以及处理器重排序的约束规则)
如果Java内存模型中所有的有序性都依靠volatile和synchronized来完成,那么有很多操作将会变得十分繁琐,其实我们编写并发代码的时候并没有考虑这一点,正是因为happens-before的规则的存在。
它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操作是否可能存在冲突的所有问题。
2. happens-before规则
- 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。注意:这里是在一个线程内,类似于as-if- serial 原则
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
- volatile 变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,具体可以看上面volatile的内存语义
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程终止规则:线程中的所有操作都先行发生与对此线程的终止检测
- 线程中断规则:对线程interrupt() 方法的调用先行发生于被中断线程的diamante检测到中断事件的发生
- 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始
- 传递性规则:如果操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C