JAVA内存模型
并发编程模型的两个关键问题
- 线程之间如何通信及线程之间如何同步;通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
Java内存模型的抽象结构
- Java 线程之间的通信由 Java 内存模型(本文简称为 JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读 / 写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java 内存模型的抽象示意图如下:
- 从图中我们可以看出,如果线程A与线程B之间要通信的话,必须要经历下面两个步骤;
-
- 线程A把本地内存A中更新过的共享变量刷新到主内存中。
-
- 线程B到主内存去读取线程A之前已更新过的共享变量。
- 线程B到主内存去读取线程A之前已更新过的共享变量。
- 从整体上来看,这两个步骤实质上是线程A在向线程B传递信号,而且这个过程要经过主内存。JMM通过以上的机制实现内存的可见性。
重排序问题
重排序分为三种类型:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从Java源代码到最终实际执行的机器码,会经历以下过程:
源代码 -> 编译器优化重排序 -> 指令级并行重排序 -> 内存系统重排序 -> 最终执行的机器码序列
如何防止重排序
为此,Java编译器在生成指令序列时会在适当的位置插入内存屏障指令来禁止特定类型的处理器重排序。如图所示:
顺带介绍一下与日常开发相关的Happens-Before规则:
- 程序顺序规则:一个线程的每个操作,happens-before于该线程中的任意后续操作,换句话说就是保证单线程执行的串行序列
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁进行加锁,先解锁才能获取锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:A hb B,B hb C, A hb C;
数据依赖性
如果两个操作同时访问一个共享变量,且这两个操作中一个为写操作,那么这两个操作存在数据依赖性。
名称 | 示例 | 说明 |
---|---|---|
写后读 | a=1;b = a; | 写一个变量后,再读这个位置 |
写后写 | a=1;a = 2; | 写一个变量后,再写这个位置 |
读后写 | a=b; b = 1; | 读一个变量后,再写这个位置 |
重排序对多线程的影响
class test{
int a = 0;
boolean flag = false;
public void writer(){
a = 1;//编号1
flag = true;//编号2
}
public void reader(){
if(flag){//编号3
int i = a*a;//编号4
..... // 接下来的逻辑
}
}
}
-
当正常按照代码书写的执行顺序去执行的时候,结果是没有问题的;
-
针对writer的指令重排序,如果程序将 a = 1 ;与 flag = true进行了指令重排序,那么线程B在执行reader方法时判断flag为true,此时a = 0,导致reader函数接下来的代码逻辑事与愿违。
-
针对reader的指令重排序,这里可能就有人会问为啥reader会进行指令排序了,这里存在着控制的依赖关系(控制依赖影响指令序列的并行度),所以对于某些编译器与处理器这里会采用猜测执行的策略,创建一个temp提前读取变量值,然后再等控制条件满足,将temp返回给i(如下图所示)
int temp = a * a;
if(flag){
int i = temp;
}
顺序一致性的内存模型
通过锁的机制保证多线程的串行执行从而保证内存模型的一致性
class test{
int a = 0;
boolean flag = false;
public synchronized void writer(){
a = 1;
flag = true;
}
public synchronized void reader(){
if(flag){
int i = a*a;
}
}
}
每个线程的执行可以进行指令重排序优化(在不影响程序的执行结果的情况下)
volatile内存语义
volatile的特性
class test{
volatile long v1 = 0L;
public void setV1(long i){
v1 = i;
}
public void getAndInc(){
v1++;
}
public long getV1(){
return v1;
}
}
等同于synchronized版
class test{
long v1 = 0L;
public synchronized void setV1(long i){
v1 = i;
}
public synchronized void getAndInc(){
long v1 = getV1();
v1++;
setV1(v1);
}
public long getV1(){
return v1;
}
}
volatile内存语义的实现
- 在每个volatile写操作的前面插入一个StoreStore屏障
- 在每个volatile写操作的后面插入一个StoreLoad屏障
- 在每个volatile读操作的后面插入一个LoadLoad屏障
- 在每个volatile读操作的后面插入一个LoadStore屏障
有个tips:优化器会根据语句的上下文判断是否需要添加内存屏障
参考书籍《Java并发编程的艺术》