java虚拟机规范中试图定义一种java内存模型(JMM)来屏蔽掉各种硬件和操作系统内存访问差异,以实现让java程序在各种平台都能打到一致的内存访问效果.所以java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层实现细节.注意,这里的变量是包括了实例字段,众泰字段,构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的,不会被共享.
1.主内存和工作内存
JMM规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存.工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存的变量,工作内存是线程私有的.这里指的主内存和工作内存,和java的虚拟机的内存划分不是同一个层次的划分,两者不对等,如果非要对等起来,主内存应该对于java堆中对象的数据区域,工作内存对于线程私有的虚拟机栈的一部分.
2.主内存与工作内存的交互
主内存与工作内存之间如何交互,java内存模型定义了以下8个操作,每个操作都是原子的,不可再分的(对于double,long可能有例外,其在有些平台上会以32位为单位读取)
1) lock:作用于主内存中的变量,它将一个变量标志为一个线程独占的状态。
2) unlock:作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。
3) read:作用于主内存中的变量,它把一个变量的值从主内存中传递到工作内存,以便进行下一步的load操作。
4) load:作用于工作内存中的变量,它把read操作传递来的变量值放到工作内存中的变量副本中。
5) use:作用于工作内存中的变量,这个操作把变量副本中的值传递给执行引擎。当执行需要使用到变量值的字节码指令的时候就会执行这个操作。
6) assign:作用于工作内存中的变量,接收执行引擎传递过来的值,将其赋给工作内存中的变量。当执行赋值的字节码指令的时候就会执行这个操作。
7) store:作用于工作内存中的变量,它把工作内存中的值传递到主内存中来,以便进行下一步write操作。
8) write:作用于主内存中的变量,它把store传递过来的值放到主内存的变量中。
在将变量从主内存读取到工作内存中,必须顺序执行read、load;要将变量从工作内存同步回主内存中,必须顺序执行store、write。并且这8种操作必须遵循以下规则:
1) 不允许read和load、store和write操作之一单独出现。即不允许一个变量从主内存被读取了,但是工作内存不接受,或者从工作内存回写了但是主内存不接受。
2) 不允许一个线程丢弃它最近的一个assign操作,即变量在工作内存被更改后必须同步改更改回主内存。
3) 工作内存中的变量在没有执行过assign操作时,不允许无意义的同步回主内存。
4) 在执行use前必须已执行load,在执行store前必须已执行assign。
5) 一个变量在同一时刻只允许一个线程对其执行lock操作,一个线程可以对同一个变量执行多次lock,但必须执行相同次数的unlock操作才可解锁。
6) 一个线程在lock一个变量的时候,将会清空工作内存中的此变量的值,执行引擎在use前必须重新read和load。
7) 线程不允许unlock其他线程的lock操作。并且unlock操作必须是在本线程的lock操作之后。
8) 在执行unlock之前,必须首先执行了store和write操作。
对于普通变量来说,他的读写操作如下:
变量读:
1) 进入同步块时,同步块中读变量的值时,将会从主存中刷新到工作区,读到最新值 2) 读volatile变量时,将会从主存中刷新到工作区,读到最新值; 3) 读普通变量时,如果跟随在读volatile变量之后,将会从主存刷新到工作区,读到最新值 4) 读final变量时,如果未初始化完成,则将等待final变量完成初始化,并获取主存中的最新值 5) 新建线程中,变量的值将会从主存刷新到工作区,读到最新值 变量写: 1) 退出同步块时,同步块中变量的赋值被强制刷新到主存; 2) volatile变量的赋值,被强制刷新到主存,时机在读volatile变量之前; 3) 普通变量随同volatile变量,在同一个线程中的赋值,将跟随volatile变量被刷新到主存; 4) final变量的赋值,在读final变量之前将被强制刷新到主存; 5) 单独的普通变量的赋值,将在线程结束之前被刷新到主存;
3.volatitle变量的特殊规则
volatile是java虚拟机提供的最轻量级的同步机制,volatile变量具有两个特性:一个是保证此变量对所有线程的可见性,即如果该变量的值被修改,这个新值对于其他线程来说是立即得知的.另一个是volatile会阻止指令重排序.由于volatile变量第一个特性,导致很多人认为volatile变量的是绝对的线程安全的,这个想法是有问题的,比如下面这个代码
1 private static volatile int a=0; 2 3 private static void increase(){ 4 a++; 5 } 6 7 public static void main(String[] args) { 8 Thread[] threads=new Thread[20]; 9 for(int i=0;i<20;i++){ 10 threads[i]=new Thread(new Runnable() { 11 @Override 12 public void run() { 13 for(int i=0;i<10000;i++) 14 increase(); 15 } 16 }); 17 threads[i].start(); 18 } 19 while(Thread.activeCount()>1) 20 Thread.yield(); 21 System.out.println(a); 22 }
这里开启了20个线程,每个线程对a变量执行10000次加一操作,如果volatile变量是绝对的线程安全的,那么这个程序输出的结果必然为200000,但是这个程序输出的结果基本上是低于200000的.原因是,实现volatile变量的可见性的方法实际上是线程每次读这个变量的时候都会从主内存中读取这个值,每次写这个变量时写完会强制刷新到主内存中.在一个线程读取了这个值之后,另一个线程修改了主内存中的值,那么前一个线程的值就变成了过期的值,所以结果会出现误差.
由于volatile变量只能保证可变性,所以在一些情况下还是需要加锁来保证原子性,在以下情况下能够不加锁使用volatile变量来保证原子性:
1) 运算结果并不依赖变量当前的值,或者确保只有单个线程可以更新变量的值.
2) 变量不需要与其他的状态变量共同参与不变约束
4.java内存模型的特征
1) 原子性:java内存模型直接保证原子性变量操作的包括read,load,assign,use,store,write,大致的课一认为基本数据类型的读写是具备原子性的(double跟long除外,开头已经说了)
实现大范围的原子性可使用lock与unlock操作.
2) 可见性:当一个线程修改了共享变量的值其他线程能够立即得知这个修改,变量如何实现可见性前面已经提到.
3) 有序性:java程序中天然的有序性可以总结为:如果本线程内观察,所有操作都是有序的,如果一个线程中观察另一个线程,所有操作都是无序的.
5.先行先发生原则
先行发生原则–是判断是否存在数据竞争、线程是否安全的主要依据。
先行发生是Java内存模型中定义的两项操作之间的偏序关系。如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响被操作B察觉。
下面是JAVA内存模型下一些天然的先行发生关系,不需要热河同步器协助就已经存在,虚拟机可以对他门进行重排序.
1、程序次序规则:在一个线程内,书写在前面的代码先行发生于后面的。确切地说应该是,按照程序的控制流顺序,因为存在一些分支结构。
2、Volatile变量规则:对一个volatile修饰的变量,对他的写操作先行发生于读操作.
3、线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作.
4、线程终止规则:线程的所有操作都先行发生于对此线程的终止检测.
5、线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码所检测到的中断事件.
6、对象终止规则:一个对象的初始化完成(构造函数之行结束)先行发生于发的finilize()方法的开始.
7、传递性:A先行发生B,B先行发生C,那么,A先行发生C.
8、管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作.