Java内存模型
计算机物理内存
CPU在计算中,不仅仅只有计算,还有对内存中数据的交互.但是因为CPU的计算速度远远大于内存数据读写速度,为了提高CPU的运行效率,不得不在CPU中增加一个高速缓存来解决CPU与内次你速度不匹配的问题.这块内存的空间比较小,无法将所有内存中的数据全部载入高速缓存计算.与此同时也带来了一个缓存一致性问题.为了解决这个问题,引入了一些缓存一致性的协议来解决这个问题.
Java内存模型(Java Memory Model,JMM)
Java内存模型与jvm内存模型是完全不一样的.Java虚拟机中定义了一种Java内存模型,用来屏蔽各种硬件和操作系统的内存访问差异,让Java程序在各种平台下都能达到一样的内存访问效果.
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量的细节.这里的变量包括了实例字段,静态字段和构成数组对象的元素,但不包括局部变量和方法参数.因为后者是私有的.
Java内存模型与硬件的内存模型很像.在Java内存模型中变量都存储在主内存(Main Memory,类比于物理内存)中.每个线程有自己的工作内存(Working Memory,类比于高速缓存),在内部保存着该线程使用的变量的主内存副本拷贝,线程对变量的操作(读取,赋值等)都在工作内存中完成.不同线程之前不能相互读取对方工作内存中的数据,线程之前变量值的传递只能通过主内存.
内存交互操作
操作 | 作用位置 | 作用 |
---|---|---|
lock(锁定) | 主内存变量 | 把一个变量标识为一个线程独占状态 |
unlock(解锁) | 主内存变量 | 把一个变量从锁定状态解除(解除之后才能被其他线程lock) |
read(读取) | 主内存变量 | 把一个变量从主内存输送到工作内存,以后后续load操作使用 |
load(载入) | 工作内存变量 | 把read操作从主内存得到的变量放入到工作内存的变量副本中 |
use(使用) | 工作内存变量 | 把内存中的变量传递给执行引擎,当字节码指令需要该值时,就会使用该命令 |
assign(赋值) | 工作内存变量 | 从执行引擎接收到的值赋值给工作内存变量 |
store(存储) | 工作内存 | 把工作内存中的变量传送到主内存中,以便后续write使用 |
write(写入) | 主内存变量 | 把从store操作过来的变量的值放入主内存中的变量 |
如何从主内存拷贝数据到工作内存,如何从工作内存拷贝到主内存等这些细节如何实现,Java内存模型定义了上面表格中的8种操作来完成.上面的8中操作还不能完全解决主内存和工作内存同步的问题,还需要下面几条规则约束才能完成.
- 不允许read,load和store,write单独出现.简单点说就是不能从主内存读了数据但工作内存不接受,或者从工作内存写数据但工作内存不接受.
- 不允许一个线程丢弃最近的assign操作.即变量在工作内存中改变了之后必须写回主内存.
- 不允许一个线程无原因的(没有发生过assign)将值写回主内存.
- 一个变量只能在主内存中创建,不允许在工作内存中使用一个未被初始化的变量.
- 一个变量在同一时刻只允许一个线程对其进行lock操作,但是同一个线程可以执行多次lock操作.在unlock时,必须执行相同次数才能解除lock操作.
- 如果对一个变量执行lock操作,它会清空该变量在工作空间的值,在执行引擎使用前,需要进行load或者assign操作来初始化变量的值.
- 如果一个变量没有被一个变量执行lock操作,那么不允许该该线程对它执行unlock操作.同时也不允许去unlock一个被别的线程lock的变量.
- 对一个变量执行unlock时,必须将工作内存的值写回主内存.即需要执行store和write.
原子性可见性和有序性
原子性
在Java内存模型中,基本数据类型的读写是具有原子性的.但是在Java内存模型中,允许虚拟机在对没有用volatile修饰的64位数据类型划分为两次32位操作来处理.即允许load,store,read和write在处理64位数据时不保证原子性.如果在多线程的情况下,在读取或者写入64位的数据类型值时只成功一半,导致程序异常.但是在现在的商业虚拟机中,很少会出现该情况.具体参照:17.7. Non-Atomic Treatment of double and long
某些场景下,需要更多大范围的原子性保证,虚拟机提供了lock和unlock来保证,另外还提供了更加高级的monitorenter和monitorexit来保证原子性.这两个关键字反映到Java代码中就是synchronized关键字,所在synchronized之间的代码能保证原子性.
可见性
可见性是指一个线程修改一个变量的值,另一个线程能够知道该值被修改.Java内存模型是通过变量值修改后将值写回主内存,读取值时从主内存读取来实现可见性的.无论是volatile修饰的变量还是普通变量.不同的在于使用volatile修饰的变量在读取时能立即从主内存中读取值,写入的时候能立刻同步回主内存.
除了volatile修饰的变量能实现可见性,另外synchronized和final也能.因为在synchronized执行unlock的时候需要先执行store和write,即将值写回主内存.而finnal修饰的字段因为声明之后无法修改,所以也是具有可见性的.
有序性
在Java内存模型中,同一个线程内部观察,所有操作是有序的.但是在另外一个线程内观察,它是无序的.Java语言提供了volatile和synchronized两个关键字来保证程序之间的有序性.volatile本身就提供了禁止指定排序的语义,而synchronized因为只会有一个线程执行lock操作,而持有同一个锁的两个同步块只能串行执行.
先行发生原则(happens-before)
上面说的有序性可以通过使用volatile和synchronized来完成,但是如果所有的都通过这种方式来完成,那么将会非常麻烦.所在在Java内存模型中定义了先行发生原则(happens-before),通过它可以来判断数据是否存在竞争,线程是否安全等问题.
什么是先行发生原则?如果A操作现行与B操作,就是说A操作发生在B操作前面,那么A操作所产生的影响能被B观察到.
在Java内存模型中已经预先定义了8条规则,这些规则不需要经过特殊编码就已经存在了.如果两个操作不在这些规则中,那么虚拟机可以随意对它们进行重新排序.
程序次序规则(Program Order Rule):在一个线程内,按照程序书写顺序,书写在前面的操作先行发生于书写在后面的操作.
i = 1;
j = i;
在同一个线程内,A操作先行发生于B操作,那么最后j的值一定为1.
管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作.需要注意的是必须是同一个锁.
class A {
private Integer number = 0;
private final Object lockA = new Object();
private final Object lockB = new Object();
public void method1(){
synchronized (lockA){
number = number+1;
}
}
public void method2(){
synchronized (lockA){
number = number+2;
}
}
public void method3(){
synchronized (lockB){
number = number+2;
}
}
}
现在创建A类型的实例a.线程t1调用method1,t2调用method2.如果某个时刻t1执行完,接着执行t2,那么在t2中,number的值一定为1,执行完t2之后,number的值一定为3.因为他们公用的是相同的锁.如果换成t2执行method3,那么执行完t2,number的值是无法确定的,因为t1和t2使用的不是用一把锁,所以不满足该条件.
volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于对这个变量的读操作.
简单点说就是使用volatile修饰的变量,即使在多线程中,只要有线程修改了volatile的值,那么在其他线程中,修改后的值能立马被读取出来.
线程启动规则(Thread Start Rule):Thread对象的start()方法先发生于该线程内的每一个动作.
线程终止规则(Thread Termination Rule):线程中所有操作都先行发生于对此程序的终止诊断.
对象终结规则(Finalizer Rule):一个对象的初始化函数,构造函数先行发生于它的finalizer方法.
传递性规则(Transitivity):如果A操作先行发生于操作B,操作B先行发生于操作C,那么就可以得出A先行发生于C操作.