Java内存模型
1. 并发编程模型的两个关键问题
在并发编程中,需要处理两个关键问题:(这里的线程指的是并发执行的活动实体)
a. 线程之间如何通信?
在命令式编程中,线程之间的通信机制有两种:共享内存 和 消息传递
b. 线程之间如何同步?
在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息显示进行通信。
在共享内存并发模型中,同步是显示进行的。
程序员必须显式指定某个方法或某段代码需要在代码之间互斥执行。
在消息传递的并发模型中,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明(看不见的..)。
2. Java内存模型的抽象结构
在Java中,所有的实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。
(在JVM内存中,堆是全局共享的)
(下面用"共享变量"这个术语代指 实例域、静态域和数组元素)
局部变量,方法定义参数和异常处理器参数不会再线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
Java线程之间的通信由Java内存模型(以下简称JMM)控制,JMM决定一个线程对共享变量写入后,何时对另一个线程可见。
从抽象角度来看,JMM定义了线程和主内存之间的抽象关系:
线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程读/写共享变量的副本。
本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
Java内存模型的抽象示意图:
从上图来看,如果线程A 与 线程B 之间要通信的话,必须经历一下两个步骤:
a. 线程A把本地内存A更新过的共享变量刷新到主内存中去。
b. 线程B到主内存中去读取线程A之前已经更新过的共享变量。
通过下面这图说名两个步骤:
这里直接贴图了
从整体来看,这两个步骤实质上是:
线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。
JMM通过控制主内存与每个线程的本地内存之间的交互,来为我们提供内存可见性保证。
3. 从源代码到指令序列的重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
指令序列是什么?还记得在上一篇文章开头写的嘛?
Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM种,JVM执行字节码,最终需要转化为汇编指令在CPU上执行
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:
a. 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
b. 指令级重排序:现代处理器采用了指令级并行技术,将多条指令重叠执行。如果不存在数据依赖,处理器可以改变语句对应机器指令的执行顺序。
c. 内存系统的重排序:由于处理其使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如下图:
上述a属于编译器重排序,b和c属于处理器重排序。
这些重排序可能会导致多线程程序出现内存可见性问题。
(了解就好,往下不再深扒)