Java内存模型
并发编程模型中的两个关键问题
在并发编程中有两个关键的问题:
- 线程之间如何通信
- 线程之间如何同步
其中通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。Android中的Handler就是属于消息传递。
在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存的公共状态进行隐式通信。在消息传递的模型里,线程没有公共的状态,线程之间必须通过发送消息来进行通信。
同步是指程序中用于控制不同线程操作发生的相对顺序的机制。在共享内存并发模型里,同步是显示进行的。必须显示的指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的模型里,由于消息的传递的发送必须在消息的接收之前,因此同步是隐式进行的。
Java采用的并发模型是共享内存的模型。
Java内存模型的抽象结构
在Java中,所有的实例域,静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。而这些被共享的变量一般被称为共享变量。局部变量,方法定义的参数和异常处理器的参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
也就是说:无状态的类是安全的。
Java内存模型简称JMM,Java线程之间的通信由JMM控制,JMM决定了一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:
线程的共享变量存储在主内存中(Main Memory),每一个线程都有一个私有的本地内存(Local Memory),本地内存中存储了线程以读/写共享变量的副本。本地内存是一个JMM的抽象概念,并不真实存在。它涵盖了缓存、写缓存区、寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:
如果线程A与B之间需要通信的话,必须要经历下面的两个步骤:
- 线程A把本地内存A中更新过的共享变量刷新到主存中去。
- 线程B到主存中去读取线程A之前已经更新过的共享变量。
从源码到执行序列的重排序
在执行程序的时候,为了提供性能,编译器和处理器常常会对执行做重排序。重排序分为三种类型。
- 编译器优化重排序。编译器在不改变单线程语言的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,那么可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理机使用缓存读/写缓冲区,这使得加载和存储操作看起来实在乱序执行。
从Java源代码到最终执行的指令序列,会经过下面的3重排序。
1属于编译器重排序,2和3属于处理器重排序。这些重排序问题可能会导致出现内存可见性的问题。
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列的时,插入特定的内存屏障(Memory Barriers)指令,通过这些内存屏障了来禁止特定类型的处理器重排序。
内存屏障是一组计算机指令,用于实现对内存操作的顺序限制。
并发编程的模型分类
现代的处理器使用缓冲区临时保存向内存写入的数据。写缓存冲可以保证执行流水线持续运行,它可以避免由于处理器停顿下来向内存写入数据而产生的延迟。同时可以通过批处理的方法刷新缓冲区,以及合并写缓冲区中对同一地址的多次写,减少对内存总线的占用。
但是有一个问题,每个处理器上的写缓冲区仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生影响:处理器对内存的读/写操作的执行顺序,不一定与内存发生的实际的读写顺序一致。
为了保证可见性,Java编译器会在生成指令的适当位置会插入内存屏障的指令来禁止特定类型的处理器重排序。JMM吧内存屏障分为4类。
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad屏障 | Load1; LoadLoad; Load2 | 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 |
StoreStore屏障 | Store1; StoreStore; Store2 | 在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 |
LoadStore屏障 | Load1; LoadStore; Store2 | 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 |
StoreLoad屏障 | Store1; StoreLoad; Load2 | 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 |
StoreLoad屏障是一个“全能型”的屏障。它同时具有其他三个屏障的效果。
happens-before简介
从jdk1.5开始,Java使用新的JSR-133内存模型。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JVM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作操作之间必须存在happens-before关系,这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
happens-before规则如下:
1、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。
2、管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。
3、volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
4、线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
5、线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
6、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
7、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
8、传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。
一个happens-before规则对应与一个或多个编译器和处理器重排序规则。