Java内存模型简介
Java内存模型主要由三部分组成:1个主内存、n个线程、n个工作内存(与线程一一对应)。数据就在三者之间倒腾。靠的是Java提供的8个原子操作:lock、unlock、read、load、use、assign、store、write。
一个变量从主内存拷贝到工作内存,再从工作内存同步回主内存的流程为:
|主内存| -> read -> load -> |工作内存| -> use -> |Java线程| -> assign -> |工作内存| -> store -> write -> |主内存|
Java内存模型中的8个原子操作
- lock:作用于主内存,把一个变量标识为一个线程独占的状态。
- unlock:作用于主内存,释放一个处于锁定状态的变量。
- read:作用于主内存,把一个变量的值从主内存传输到线程工作内存中,供之后的load操作使用。
- load:作用于工作内存,把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use:作用于工作内存,把工作内存中的一个变量传递给执行引擎,虚拟机遇到使用变量值的字节码指令时会执行。
- assign:作用于工作内存,把一个从执行引擎得到的值赋给工作内存的变量
- store:作用于工作内存,把工作内存中的一个变量传送到主内存中,供之后write操作使用。
- write:作用于主内存,把store操作从工作内存中得到的变量值存入主内存的变量中。
八个原子操作的执行过程
有关变量拷贝过程的规则
- 不允许read和load,store和write单独出现。
- 不允许线程丢弃它最近的assign操作,即工作内存变化之后必须把该变化同步回主内存中。
- 不允许一个线程没有assign的情况下将工作内存同步回主内存中,也就是说,只有虚拟机遇到变量赋值的字节码时才会将工作内存同步回主内存。
- 新的变量只能从主内存中诞生,即不能在工作内存中使用未被load和assign的变量,一个变量在use和store前一定先经过了load和assign。
有关加锁的规则
- 一个变量在同一时刻只允许一个线程对其进行lock操作,但是可以被一个线程多次lock(锁的可重入)。
- 对一个变量进行lock操作会清空这个变量在工作内存中的值,然后在执行引擎使用这个变量时,需要通过assign或load重新对这个变量进行初始化。
- 对一个变量执行unlock时,必须将该变量同步回主内存中,即执行store和write操作。
- 一个变量没有被lock,就不能被unlock,也不能去unlock一个被其他线程lock的变量。
可见性问题->有序性问题
Java线程只能在自己的工作内存工作,其对变量的所有操作(读取、赋值等)都必须在工作内存中使用,不能直接读写主内存中的变量。这就有可能会导致可见性问题:
-
因为对于主内存中的变量A,其在不同的线程的工作内存中可能存在不同的副本A1、A2、A3。
-
不同线程的read和load、store和write不一定是连续执行的,中间可以插入其他命令。Java只能保证read和load、store和write的执行对于一个线程而言是连续的,但是并不保证不同线程的read和load、store和write的执行顺序是连续的。
- 假设有两个线程A和B,其在线程A在写入共享变量,线程B要读取共享变量,我们想让线程A先完成写入,线程B再完成读取。此时即使我们按照“线程A->线程B读取”的顺序开始执行的,真实的执行顺序也可能是这样的:storeA->readB->writeA->loadB,这将导致线程B读取到的是变量的旧值,而非线程A修改过的新值。也就是说,线程A修改变量的执行先于线程B操作了,但这个操作对于线程B而言依旧是不可见的。
通过上述问题分析,可见性问题的本身,也是由于不同线程之间的执行顺序得不到保证导致的,因此我们也可以将它的解决和有序性合并,即对Java一些指令的操作顺序进行限制,这样既保证了有序性,又解决了可见性。
于是,Java给出了命令执行的顺序规范:Happens-Before规则。
Happens-Before规则
Happens-Before,就是即便是对于不同的线程,前面的操作也应该发生在后面操作的前面,也就是说,Happens-Before规则保证:前面的操作的结果对后面的操作一定是可见的。
Happens-Before规则本质上是一种顺序约束规范,用来约束编译器的优化行为。就是说,为了执行效率,我们允许编译器的优化行为,但是为了保证程序运行的正确性,我们要求编译器优化后需要满足Happens—Before规则。
根据类别,将Happens-Before规则分为了以下4类:
-
操作的顺序:
- 程序顺序规则:如果代码中操作A在操作B之前,那么同一个线程中A操作一定在B操作前执行,即在本线程内观察,所有操作就是有序的。
- 传递性:在同一个线程中,如果A先于B,B先于C那么A必然先于C。
-
锁和volatile:
- 监视器锁规则:监视器锁的解锁操作必须在同一个监视器锁的加锁操作前执行。
- volatile变量规则:对volatile变量的写操作必须在对该变量的读操作前执行,保证时刻读取到这个变量的最新值。
-
线程和中断:
- 线程启动规则:Thread#start()方法一定先于线程中执行的操作。
- 线程结束规则:线程的所有操作先于线程的终结。
- 中断规则:假设有线程A,其他线程interruptA的操作先于检测A线程是否中断的操作,即对一个线程的interrupt()操作和interrupted()等检测中断的操作同时发生,那么interrupt()先执行。
-
对象生命周期相关:
- 终极器规则:对象的构造函数执行先于finalize()方法。
volatile的实现原理
volatile变量有以下两个特点:
- 保证对所有线程的可见性。
- 禁止指令重排序优化。
Happens-Before规则中要求,对volatile变量的写操作必须在对该变量的读操作前执行。解决方法分两步:
- 保证动作发生;
- 保证动作按正确的顺序发生。
- 保证动作发生
首先,在对volatile变量进行读取和写入操作,必须去主内存拉去最新值,或是将最新值更新进主内存,不能只更新进工作内存而不将操作同步进主内存,即在执行read、load、use、assign、store、write操作时。
-
use操作必须与load、read操作同时出现,不能只use,不load、read。
- use<-laod<-read
-
assign操作必须与store、write操作同时出现,不能只assign,不store、write。
- assign->store->write
此时,已经保证了将变量的最新值时刻同步进主内存的动作发生了,接下来,我们需要保证这个动作,对于不同的线程,满足volatile的Happens-Before规则:对变量的写操作必须在对该变量的读操作前执行。
- 保证动作按正确的顺序发生
导致执行顺序问题的主要原因在于,这个读写volatile变量的操作不是一气呵成的,它不是原子的。无论是读还是写,都被分成了三个命令(use<-laod<-read或assign->store->write),这就导致了,能够保证assignA发生在useB之前,但不能保证writeA也发生在useB之前,而如果writeA不发生在useB之前,主内存中的数据就是旧的,线程B就读不到最新值。
假设是一个写操作,你发生在我之前的读操作可以随便执行,各个分解命令先于我还是后于我都无所谓。但是,发生在我之后的读操作,必须等我把3个命令都执行完,才能执行。
volatile的真实实现:
利用了lock操作的特点,volatile在执行变量赋值操作之后,额外加了一行
lock add $0x0,(%esp)
给ESP加上寄存器+0,这是一个无意义的空操作,重点在lock上:
-
保证动作发生:
- lock指令会将当前CPU的Cache写入内存,并无效化其他CPU的Cache,相当于在执行了assign之后,又进行了store->write。
- 这使得其他CPU可以立即看见volatile变量的修改,因为其他CPU在读取volatile变量时,会发现自己的缓存过期了,于是会去主内存中拉去最新的volatile变量值,也就被迫在use前进行了一次read->load。
-
保证动作顺序:
- lock的存在相当于一个内存屏障,使得在重排序时,不能把后面的指令排在内存屏障之前。