JAVA内存模型的基础
在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
- 在共享内存的并发模型中,线程之间共享程序的公共状态,通过读写内存中的公共状态来进行隐式通信。
- 在消息传递的并发模型中,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。
JAVA的并发采用的是共享内存模型。
JAVA内存模型的抽象结构
- 在JAVA中,所有实例域、静态域和数组元素都在堆内存中,堆内存在线程之间共享。
- 局部变量、方法定义参数和异常处理函数,在栈内存里面,不会在线程之间共享。
线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存存储了该内存以读写共享变量的副本。
A与B之间的通信,需要通过以下2个步骤
- 线程A把本地内存A中更新过的共享变量刷新的主内存中。
- 线程B到主内存中去读取线程A之前已经更新过的共享变量。
从源代码到指令序列的重排序
在执行程序时,为了提高性能,编译器和处理器通常会对指令做重排序,重排序分为三种:
- 编译器优化的重排序:编译器在不改变单线程语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应的机器指令的执行顺序。
- 内存系统的重排序:由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是乱序执行。
对于指令重排,JAVA内存模型的处理器重排序规则可以要求JAVA编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。
内存屏障:指的是重排序时不能把后面的指令重排序到内存屏障之前的位置。
展示一个由于内存操作重排带来问题的例子(经典)
在展示之前,首先补充一点知识:现代处理器使用写缓存区临时保存向内存下入的数据,以保证指令流水线持续运行,避免处理器停顿下来等待向内存写入数据而产生延迟,同时,以批处理的方式刷新缓存区,以及合并写缓存区对统一内存地址的多次写,减少堆内存总线的占用。
这个特性会对内存操作的顺序产生影响:处理器堆内存的读写操作的执行顺序,不一定与内存实际发生的读写操作顺序一致(因为内存操作重排)
如果AB同时执行,可能会得到读取到脏数据,原因如下:
此处由于指令重排,导致A1->A2的顺序变成了A2->A1,所以读取了脏数据。
一般情况下,处理器都不允许对存在数据依赖的操作做重排序。
happens-before
在JMM中,如果一个操作的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。
A happens-before B, JMM 并不要求A一定要在B之前执行, JMM仅仅要求前一个操作的结果对后一个操作课件,且前一个操作按顺序排在第二个操作之前.