要了解Java内存模型,首先我们要了解什么是Java内存模型,它有什么作用?
描述Java内存模型(简称:JMM)的规范提案JSR-133标题《Java Memory Model and Thread Specification》,通过这个标题,可以看出JMM是和线程相关的规范。此规范地指定的 JMM Web Site 上对规范的说明如下:
The Java Memory Model defines how threads interact through memory.
通过以上描述,说明JMM规范主要是解决在多线程场景下线程间如何通信。
硬件内存架构
要了解JMM,我们先来从硬件角度,看看多核CPU场景下,多线程程序会存在什么问题。
如上图所示,在多核(多CPU)硬件架构中,系统中有两个CPU,分布运行了一个线程,对象obj保存在主内存(RAM)中。由于RAM的速度远低于CPU,为了加快数据的访问,当CPU(线程)需要使用obj对象时,会预先把obj对象加载到CPU的缓存(CPU Cache)中,处理完毕后,再把对obj对象的更新回写到到RAM。
每个CPU有自己独立的缓存,一个CPU无法访问其他CPU的缓存,也就是CPU间无法直接交换数据,CPU间所有的数据交换都需要借助主内存来完成。
假设线程执行的是 +1
操作。在上图示例中,两个线程并发执行。初始状态,主内存中obj.num=1;线程1先读取了obj对象,并执行+1操作,结果obj.num=2;在线程1的修改还未从CPU缓存回写到主内存的时候,线程2从主内存中读取了obj对象,此时线程2读取到的obj.num=1;此后,线程1和线程2分别把obj回写到主内存;按正常业务逻辑,obj.num被+1了两次,结果应该是3,但上述情况,最终主内存中obj.num=2。这是因为两个线程对数据并发访问冲突导致线程读到的数据不一致。
Java内存模型
Java是平台无关的语言,为了实现跨平台运行,Java虚拟机(JVM)上运行的是Java字节码(Java bytecode)。Java内存模型(Java Memory Model,JMM)是Java虚拟机规范定义的,用来屏蔽掉Java程序在各种不同的硬件和操作系统对内存的访问的差异,实现Java程序在各种不同的平台上都能达到内存访问的一致性。和硬件内存架构类似,JMM把内存分为 主内存 和 工作内存 ,主内存由所有线程共享,工作内存为线程私有。
JMM规范主要定义程序变量操作的规则,规范中定义的主内存、工作内存的概念和JVM运行时内存分区中定义的堆、栈区域不是同一纬度的概念,不能互相对应,不过为了便于理解,可把主内存类比为堆,工作内存类比为栈。
虽然工作内存和栈可以类比,但两者是不同的概念。
JMM管理的程序变量,主要是指在对象实例字段、静态字段、构成数组字段的元素等,不包括方法参数、方法局部变量等保存在栈里的变量,因为栈本身就是线程私有的,并不存在线程一致性问题。
JMM规范规定所有的变量都要在主内存中产生,而线程不允许直接操作主内存中的变量,线程需要把变量副本拷贝到工作线程中进行操作,操作完后再回写到主内存。
主内存
JMM规定所有的变量都必须在主内存中产生。
工作内存
JVM中每个线程都有自己的工作内存,是线程私有的,可以类比CPU的高速缓存。线程的工作内存保存了线程需要的变量在主内存中的副本。
数据交互接口
JMM中定义了8个用于主内存和工作内存见数据互操作的接口,用于在两者间传输数据,这些操作都是原子性的。
- lock(锁定)
作用于主内存变量,属于互斥锁,一个变量同时只能一个线程锁定
- unlock(解锁)
作用于主内存变量,lock的反操作,释放变量的锁
- read(读取)
作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
- load(载入)
作用于线程工作内存变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中
- use(使用)
作用于线程工作内存变量,表示把工作内存中的一个变量的值传递给字节码指令
- assign(赋值)
作用于线程工作内存变量,表示把字节码指令执行返回的结果赋值给工作内存中的变量,字节码赋值操作
- store(存储)
作用于线程工作内存变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
- write(写入)
作用于主内存变量,把store操作从工作内存中得到的变量的值放入主内存的变量中
数据交互原则
- 变量只能在主内存中产生。
- 线程对主内存变量的操作必须在线程的工作内存中进行,不能直接操作主内存中的变量。
- 不同的线程之间也不能相互访问对方的工作内存。线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。
- read和load操作、store和write必须成对使用,即:不允许从主内存中读取了变量,工作内存不接收,或者工作内存回写了变量,主内存不接收。
- assign操作后的变量必须回写到主内存。
- 不允许回写没有修改(即未assign)的变量到主内存。
- 一个变量同时只能被一个线程对其进行lock操作,但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同。
- 对变量执行lock操作,就会清空工作空间该变量的值,使用时需要重新读取;对一个变量执行unlock之前,必须先把变量同步回主内存中。
内存并发一致性原则
上述的内存并发一致性问题,在JMM中定义了三个原则来避免,分别是原子性、可见性和有序性。
原子性
可见性
可见性是指一个线程修改了一个变量的值后,其他线程立即可以读取到这个变量的最新值。
有序性
为了使处理器内部运算单元尽可能被充分利用,处理器还会对输入的代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在乱序执行之后的结果进行重组,保证结果的正确性,也就是保证结果与顺序执行的结果一致。但是在真正的执行过程中,代码执行的顺序并不一定按照代码的书写顺序来执行,可能和代码的书写顺序不同。
参考资料
- JSR-133: JavaTM Memory Model and Thread Specification
- The Java Memory Model