Java内存模型(JMM)定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
在Java中,所有实例域、静态域和数组元素都存在堆内存中,堆内存在线程之间共享,这些变量就是共享变量。
局部变量(Local Variables),方法定义参数(Formal Method Parameters)和异常处理参数(Exception Handler Parameters)不会在线程之间共享,它们不存在内存可见性问题。
上图是抽象结构,一个包含共享变量的主内存(Main Memory),出于提高效率,每个线程的本地内存中都拥有共享变量的副本。Java内存模型(简称JMM)定义了线程和主内存之间的抽象关系,抽象意味着并不具体存在,还涵盖了其他具体的部分,如缓存、写缓存区、寄存器等。
此时线程A、B之间是如何进行通信的呢?
明确一点,JMM通过控制主内存与每个线程的本地内存之间的交互,确保内存的可见性。
编译器和处理器为了优化程序性能会对指令序列进行重新排序,重排序可能会导致多线程出现内存可见性问题。
JMM对于编译器重排序规则会禁止特定类型的编译器重排序。
对于处理器重排序,JMM的处理器重排序会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,以禁止特定类型的处理器重排序。
如果两个操作访问同一变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。
编译器和处理器会遵守数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序。(针对单个处理器中执行的指令序列和单个线程中执行的操作)
考虑抽象内存模型,现代处理器处理线程之间数据的传递的过程:将数据写入写缓冲区,以批处理的方式刷新写缓冲区,合并写缓冲区对同一内存地址的多次写,减少内存总线的占用。但每个写缓冲区只对它所在的处理器可见,处理器对内存的读/写操作可能就会改变。
不管怎么重排序,(单线程)程序的执行结果不能被改变,同样,不会对具有数据依赖性的操作进行重排序,相应的,如果不存在数据依赖,就会重排序。
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
很明显,as-if-serial
语义很好地保护了上述单线程,让我们以为程序就是按照A->B->C的顺序执行的。
从JDK5开始,Java使用新的JSR-133内存模型,使用happens-before
的概念阐述操作之间的内存可见性。
有个简单的例子理解所谓的可见性和happens-before“先行发生”的规则。
i = 1; //在线程A中执行
j = i; //在线程B中执行
我们对线程B中这个j的值进行分析:
假如A happens-before B,那么A操作中i=1的结果对B可见,此时j=1,是确切的。但如果他们之间不存在happens-before的关系,那么j的值是不一定为1的。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,两个操作可以在不同的线程中执行,那么这两个操作之间必须要存在happens-before。
以下源自《深入理解Java虚拟机》
意味着不遵循以下规则,编译器和处理器将会随意进行重排序。
start()
先行发生于此线程的每一个动作。interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生。finalize()
方法的开始。举个例子:
private int value = 0;
public void setValue(int value){
this.value = value;
}
public int getValue(){
return value;
}
假设此时有两个线程,A线程首先调用setValue(5)
,然后B线程调用了同一个对象的getValue
,考虑B返回的value值:
根据happens-before
的多条规则一一排查:
综上所述,最然在时间线上A操作在B操作之前发生,但是它们不满足happens-before
规则,是无法确定线程B获得的结果是啥,因此,上面的操作不是线程安全的。
如何去修改呢?我们要想办法,让两个操作满足happens-before
规则。比如:
synchronized
关键字给setValue()
和getValue()
两个方法上一把锁。volatile
关键字给value修饰,这样写操作在读之前,就不会修改value值了。考虑重排序对多线程的影响:
如果存在两个线程,A先执行writer()方法,B再执行reader()方法。
class ReorderExample {
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
……
}
}
}
在没有学习重排序相关内容前,我会毫不犹豫地觉得,运行到操作4的时候,已经读取了修改之后的a=1,i也相应的为1。但是,由于重排序的存在,结果也许会出人意料。
操作1和2,操作3和4都不存在数据依赖,编译器和处理器可以对他们重排序,将会导致多线程的原先语义出现偏差。
上面示例就存在典型的数据竞争:
我们应该保证多线程程序的正确同步,保证程序没有数据竞争。
这些机制实际上可以把所有线程的所有内存读写操作串行化。
顺序一致性内存模型和JMM对于正确同步的程序,结果是相同的。但对未同步程序,在程序顺序执行顺序上会有不同。
对于正确同步的程序(例如给方法加上synchronized关键字修饰),JMM在不改变程序执行结果的前提下,会在在临界区之内对代码进行重排序,未编译器和处理器的优化提供便利。
对于未同步或未正确同步的多线程程序,JMM提供最小安全性。
一、什么是最小安全性?
JMM保证线程读取到的值要么是之前某个线程写入的值,要么是默认值(0,false,Null)。
二、如何实现最小安全性?
JMM在堆上分配对象时,首先会对内存空间进行清零,然后才在上面分配对象。因此,在已清零的内存空间分配对象时,域的默认初始化已经完成(0,false,Null)
三、JMM处理非同步程序的特性?
对于单线程程序和正确同步的多线程程序,只要不改变程序的执行结果,编译器和处理器无论怎么优化都OK,优化提高效率,何乐而不为。
异:as-if-serial 保证单线程内程序的结果不被改变,happens-before 保证正确同步的多线程程序的执行结果不被改变。
同:两者都是为了在不改变程序执行结果的前提下,尽可能的提高程序执行的并行度。
参考资料:
《Java并发编程的艺术》方腾飞
《深入理解Java虚拟机》周志明