Java内存模型
Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
定义Java内存模型并非一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并非内存访问操作不会产生歧义:但是,也必须定义的足够宽松,使得虚拟机的实现有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存和指令集中某些特有的指令)来获取更好的执行速度。经过长时间的验证和修补,在JDK1.5(实现了JSR-133)发布后,Java内存模型已经成熟和完善起来了。
主内存和工作内存
Java内存模型的主要目标就是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有,不会被共享,自然就不会存在竞争问题。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量,不同线程之间也无法直接访问其他线程工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者的交互关系如下图所示:
原子性、可见性与有序性
Java内存模型是围绕着在并发过程中如何处理原子性、可见性与有序性这3个特征来建立,我们逐个来看一下哪些操作实现了这3个特性。
原子性
原子性是指一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
除了long型字段和double型字段外,Java内存模型确保访问任意类型字段所对应的内存单元都是原子的。这包括引用其它对象的引用类型的字段。此外,volatile long 和volatile double也具有原子性 。
如果应用程序需要一个更大范围的原子性保证,Java内存模型还提供了显示锁和synchronized 关键字。
可见性
可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存将刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,已经每次使用前立即从主内存刷新。因此,volatile关键字可以保证多线程操作时变量的可见性。
除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final。
有序性
有序性是指 程序执行的顺序按照代码中的先后顺序执行。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。
happens-before
从JDK5开始,java使用新的JSR -133内存模型(本文除非特别说明,针对的都是JSR- 133内存模型)。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
与程序员密切相关的happens-before规则如下:
- 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
- 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
- volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
- 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。
注意,两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。