1.1 主内存和工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存中和从内存中取出变量这样的底层细节。
此处的变量和Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。
为了获得较好的执行效率,Java内存模型没有限制引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。
Java内存模型规定了所有的变量都存储在主内存中(此处的主内存仅仅指虚拟机内存的一部分),每条线程还有自己的工作内存(可与处理器高速缓存类比),线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝(可理解为从主内存中读取变量进行缓存),线程对变量的所有操作都必须在工作内存中进行,而不能直接读取主内存中的变量(volatile变量也是如此,只不过有特殊的操作性规定,看起来如同直接在主内存中读写访问一样)。不一样的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存和工作内存三者之间的关系如下图所示:
这里说的主内存,工作内存跟JVM内存无关。如果一定要勉强对应起来,从虚拟机内存定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存对应虚拟机栈中的部分区域。从硬件角度看,主内存就是物理硬件的内存,工作内存对应寄存器和高速缓存。
1.2 内存间交互操作
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8中操作来完成,虚拟机实现时必须保证下面每一种操作都是原子的、不可再分的(load、store、read和write操作在某些平台上允许有例外)。
- lock:作用于主内存的变量,它把一变量标识为一条线程独占的状态。
- unlock:作用于主内存的变量,它把一个处于锁定状态的变量释放出来。
- read:作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load:作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use:作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign:作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store:作用于工作内存中的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write:作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
如果要把一个变量从主内存复制到工作内存,那么就要顺序执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序执行store和write操作。
Java内存模型要求上述两个操作必须按顺序执行,而没有保证是连续执行。也就是说,read和load之间,store和write之间可以插入其他指令。Java内存模型还规定了在执行8中操作时必须满足如下规则:
(1) 不允许read和load,store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起了回写但主内存不接受的情况。
(2) 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中修改了之后必须把该变化同步回主内存。
(3) 不允许一个线程无原因(没有发生过任何assign操作)的把数据从线程的工作内存同步回主内存中。
(4) 一个新的变量只能在主内存中“诞生”。
(5) 一个变量同一个时刻只允许一个线程对其lock操作,但lock操作可以被同一个线程重复执行多次,多次lock后,只有执行相同次数的unlock操作,变量才会被解锁。
(6) 如果对一个变量执行lock操作,那么将会清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load或assign操作初始化变量的值。
(7) 如果一个变量事先没有被lock操作锁定,就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
(8) 对一个变量执行unlock操作之前,必须把此变量同步回主内存中。
这8种内存访问操作以及上述规则限定,再加上volatile的一些特殊规定,就完全确定了java程序中哪些内存访问操作在并发下是安全的。
volatile
Java内存模型对volatile专门定义了一些特殊的规则。volatile具备两种特性,第一是保证此变量对所有的线程的可见性。第二是volatile变量禁止指令重排序优化。
(1)特性一可以这样理解:在线程的工作内存中,volatile变量改变之后立即刷新到主内存中,每次要使用volatile变量时必须从主内存中去读取。
假设有两个线程,分别对volatile变量做读和写操作。当写操作线程对变量进行改变之后,立即将其从工作内存中刷回主内存。读操作线程每次使用变量的时候需要去主内存中读取变量,可以保证读到最新的变量值。也就是说,写操作对变量的更改是对读操作线程可见的。而对于普通变量来讲,读操作线程将变量读入到工作内存之后,就保存到工作内存中,要用的时候直接从工作内存中取,就看不到主内存里该变量值的改变。
volatile可以看做是一种轻量级的锁,但是它并不能一定保证并发的安全性。当同时有多个线程对变量做写操作时,volatile就不适合了。这是因为写操作不具有原子性。写操作大致可以分为三步,分别是将变量从主内存读入工作内存,在工作内存中对变量进行更改,将更改后的值写回主内存中。假如B线程在A线程将变量写回主内存之前读取变量到自己的工作内存中进行操作,然后B线程将工作内存中的变量写回主内存,接着A线程也将自己的工作内存的变量写回主内存,导致B的结果覆盖了A的结果。主内存中变量的最终结果将取决于线程执行的顺序,这就带来了线程并发的竞态问题。
(2)特性二是指volatile变量禁止重排优化。
在java语句执行的过程中,为了提升效率,JVM和编译器都可能对语句进行重排优化。比如语句A写在B之前,重排的时候可能先执行B,再执行A。当然重排是为了提升效率,有自己的规则而不是随便乱序重排的的。当语句执行结果之间有依赖性的时候就不能重排。比如A:a=a+3, B:a=a*10,B语句中a依赖于A语句的执行结果,(a+3) * 10 和a * 10+3的结果不相同所以不能排序。
在多线程中,假设A线程通过判断一个boolean变量值来输出另外一个变量的值,B线程先在给另一个变量赋值,再改变boolean变量的值。在A线程中可能会出现这样一种结果:A看到boolean变量的值改变了,但是看不到另一个变量的值。因为B线程中boolean变量赋值和另一个变量赋值语句发生了重排,boolean变量赋值语句执行了,而它前面的变量赋值语句还没有执行。加上volatile关键字之后,可以保证变量赋值语句不会被重排。
1.3 对于long和double型变量的特殊规则
Java内存模型要求lock、unlock、read、load、assign、use、store和write这8个操作具有原子性,但是对于64位的数据类型(long,double)在模型中特别定义了一条相对宽松的规则:允许虚拟机将没有被volatile修饰的64位数据的读写操作分为了两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这4个操作的原子性,这就是所谓的long和double的非原子性协定。
如果有多个线程共享一个未声明为volatile的long或者double类型的变量,并且同时对它们进行写操作,那么某些线程可能会读取到非修改值的代表了 “半个变量”的数值。
但是目前商用Java虚拟机中不会出现这种情况,因为Java内存模型虽然允许非原子协定,但是强烈建议把64位数据的读写操作实现为原子操作来对待(也就是说大部分虚拟机实现上,long和double是遵守原子协定的)。
1.4 原子性、可见性和有序性
Java内存模型是围绕着在并发过程中如何处理原子性,可见性和有序性这3个特征来建立的。
原子性:由Java内存模型来直接保证原子性操作包括read、load、assign、use、store和write。可以认为基本数据类型的访问读写是具备原子性的。
可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
volatie修饰的变量具有可见性是因为特殊规定了工作内存不能缓存修改,而是将修改立即同步到主内存。
sychronized关键字同样可以保证可见性,是由于unlock操作会将工作内存的变量同步到主内存中。
有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句指线程内表现为串行的语义,后半句指指令重排序现象和工作内存与主内存同步延迟现象。Java语言提供了volatile和sychronized两个关键字来保证线程之间操作的有序性。volatile本身就包含了禁止指令重排序的语义,而sychronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行进入。