重点
内存可见性、重排序、顺序一致性、volatile、锁、final
主内存和工作内存
Java内存模型主要目标用来屏蔽掉各种硬件和操作系统的内存之间的差异,以实现Java程序在各个平台下都能达到一致的n内存访问效果。Java内存模型定义了程序中变量的访问规则,即虚拟机将变量存储到内存和从内存取出比变量的规则。此处的变量是广义的,包括实例、静态变量或者数组元素,但是局部变量和方法参数是线程私有的,不包含这两个,和前边的提到变量区分开。
主内存可以理解为物理硬件的主内存,工作内存是每个线程私有的内存,可以类比成处理器的高速缓存。Java内存模型规定了所有的变量都存储在主内存中,线程的工作内存保存了被该线程使用的变量在主内存中的拷贝,规定线程对变量所有的操作(读取、赋值)都在自己的工作内存中完成,各个线程的工作内存不能相互独立 ,而线程之间通过主内存完成变量值的传递。Java线程、工作内存和主内存之间关系可以由下图表示:
(图片来源:https://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html)
内存之间的交互操作
主内存和工作内存的交互协议,Java内存模型规定了8种操作完成,每种操作都是原子的、不可再分的(long和double在某些平台允许有例外,后边会提到):
-
lock(锁定):作用于主内存的变量,它把一个变量标识为由一个线程单独占用状态。
-
unlock(解锁):作用于主内存的变量,将一个锁定的变量解锁释放,解锁释放后的变量才可以被重新锁定。
-
read(读取):作用于工作内存的变量,将一个变量从主内存中传输到线程的工作内存中去,以便于后续的load操作使用。
-
load(载入):作用于工作内存的变量,将read操作读取过来的变量存储到工作内存中的变量副本中。
-
use(使用):作用于工作内的变量,将工作内存中的变量传递给执行引擎。当虚拟机遇到一个需要使用变量的字节码指令时使用该操作。
-
assign(赋值):作用于工作内存变量,将从执行引擎接收到的值赋值给工作内存中的变量,当虚拟机遇到一个赋值操作的字节码指令时,使用该操作。
-
store(存储):作用于工作内存变量。将工作内存中的变量传递到主内存中,以便于后边的write操作使用。
-
write(写入):作用主内存的变量,将store操作传递来变量值保存到主内存中的变量中。
如果要把主内存中的变量存储到工作内存中,就要顺序的执行read和load操作,如果要把工作内存中的变量存储到主内存中,就要顺序的执行store和write操作。Java内存模型规定了这两组操作的执行顺序,但是不要求是连续执行的。除此之外,Java内存模型还有如下8种规定: -
不允许read和load、store和write操作单独出现,即read和load要么都有,要么都没有。
-
不允许一个线程丢弃它最近的assign操作,即变量被赋值之后必须同步到主内存中去。
-
不允许一个线程没有assign操作就把变量数据同步到主内存中去。
-
一个新的变量只能诞生于主内存中,即工作内存中不能直接使用一个未被初始化的变量(没有load或者assign)
-
一个变量,同一时刻,只能被一个线程做lock操作,但是一个变量可以被同一个线程多次lock,多次lock之后的变量要被同一线程执行相同次数的unlock操作才可以被解锁。
-
如果一个变量被lock后,线程中的该变量的值会被清空,如果执行引擎要使用这个变量,必须重新load或者assign。
-
如果一个变量没有被lock,那么就不允许对这个变量执行unlock操作,被lock锁定的变量不能其他线程unlock。
-
一个变量unlock之前,必须将此变量同步到主内存中去。
对于volatile型变量的特殊规则
关键字volatile是Java虚拟机提供的最轻量级的同步机制,Java内存模型对volatile变量提供了特殊的访问规则。当一个变量被定义成volatile类型之后,具备两种特性:
特性一:保证此变量对其他所有线程的可见性
可见性是指,当一个线程对此变量进行了修改,那么新的值对于其他的线程来说是立即可知的。和普通变量的区别就是,普通变量在线程之前的传递都是通过主内存来完成的。volatile关键字并不会保证一个变量在所有的线程中一致,换句话说,volatile变量不存在一致性这个说法,可能一致,也可能不一致。执行引擎使用volatile变量之前都会刷新变量,所以对于执行引擎来说,它看不到不一致的情况,所以“volatile变量的运算在并发下是安全的”这种说法是错误的,运算操作并不是原子性的,可能运算之间,其他线程对变量进行了修改,导致并发问题。
特性二:禁止指令重排序优化
和普通变量的区别:普通变量仅仅保证在方法执行的过程中,所有依赖复制结果的地方都能够获取到争取的结果,但是不能保证变量赋值的操作顺序和代码中的执行顺序一致。比如说,指令1把地址A中的值加10,指令2把地址A的值乘以2,执行3把地址B中的值减去3,这时候,指令1和指令2是有依赖关系的,指令1和指令2是不能重排序的,因为(A+10)2不等于A2+10,但是指令3和A没有关系,可以在指令1和指令2之前或者之间执行。
对于long和double类型的特殊规则
Java内存模型要求内存之间的交互的8个操作都是原子性的。但是对于64位类型(long和double),Java模型允许将没有 volatile修饰的64位类型数据的读写分为2次32位操作,即所谓的“非原子性协定”。如果多个线程共享一个未被volatile修饰的64位变量,并且对其做读写操作,那么某些线程可能会得到一个既非原值也非其他线程修改的值这种“半个变量”,但是这种情况非常罕见,起码商用虚拟机不会出现这种情况,所以在编写代码时一般不需要将long和double类型用volatile来修饰。