Java内存模型定义了程序中各个实例变量的访问规则,即在虚拟机中将变了存储到内存和从内存中取出变量这样的底层细节。它主要是来操作实例变量、静态字段及构成数组对象的元素。因为局部变量及方法参数是线程私有的不会被共享,所以JMM不会影响局部变量及方法。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
说明:1:所有变量(共享的)都存储在主内存中,每个线程都有自己的工作内存(或者称为本地内存)
2:工作内存中保存该线程使用到的变量的主内存副本拷贝
3:线程对变量的所有操作都应该在工作内存中完成
4:不同线程不能相互访问工作内存,交互数据要通过主内存
Java内存模型中定义了8中操作来完成主内存与工作内存的交互(实例变量从主内存拷贝到工作内存)。这8中操作都是原子的不可在分割的。其操作如下:
1:lock:锁定,把变量标识为线程独占,作用于主内存变量
2:unlock:解锁,把锁定的变量释放,别的线程才能使用,作用于主内存变量
3:read:读取,把变量值从主内存读取到工作内存
4:load:载入,把read独取到的值放入工作内存的变量副本中
5:use:使用,把工作内存中一个变量的值传递给执行引擎
6:assign:赋值,把从执行引擎接收到的值赋给工作内存里面的变量
7:store:存储,把工作内存中一个变量的值传递到主内存中
8:write:写入,把store进来的数据存放如主内存的变量中
以上八种操作是具有一定规则的,如下:
1:不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说, read与load之间、store与write之间是可插入其他指令的。
2:不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
3:不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
4:一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是 对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
5:一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后, 只有执行相同次数的unlock操作,变量才会被解锁。
6:如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操 作初始化变量的值。
7:如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
8:对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)
Java 内存模型是围绕着在并发过程中如何处理原子性、可见性、有序性这3个特征来建立的
1.原子性:
Java内存模型来直接保证原子性的变量操作包括read,load,assign,use,store,和write。我们大致可以认为基本 数据类型的读写是具备原子性的。(long和double例外,但无需在意这些几乎不会发生的例外情况),如果需要更大范围的原子性保证Java内存模型提供了lock和unlock操作来满足。尽管虚拟机未把lock 和unlock操作直接开放给用户。但是却提供了更高层次的字节码指令monitorenter 和 monitorexit 来隐式的使用这两个操作。其中synchronized就是使用的这两个操作。
2.可见性:
是指当一个线程修改了共享变量的值,其他线程能够立即得到这个修改。其中volatile 、synchronized、和final都具备可见性。其中volatile变量 每次线程修改了值都会同步到主内存中而其他线程在使用该变量的时候先重主内存中刷新数据到本地内存。
2.有序性:
通过volatile 和synchronized 来保证线程的有序性。其中volatile 是禁止指令重排序,而synchronized 是保证同一个时刻只有一个线程能访问某个变量或方法。
指的是JVM为了优化,在条件允许的情况下,对指令进行一定的重新排列,直接运行当前能够立即执行
的后续指令,避开获取下一条指令所需数据造成的等待。
一.指令重排序讨论的是在单线程的前提下,不考虑多线程。
二:不是所有的指令都能重拍,比如:
写后读 a = 1;b = a; | 写一个变量之后,再读这个位置 |
写后写 a = 1;a = 2; | 写一个变量之后,再写这个变量 |
读后写 a = b;b = 1; | 读一个变量之后,再写这个变量 |
以上语句不可重排,但是a=1;b=2;是可以重排的
三 指令重排的基本原则
1:程序顺序原则:一个线程内保证语义的串行性
2:volatile规则:volatile变量的写,先发生于读
3:锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
4:传递性:A先于B,B先于C 那么A必然先于C
5:线程的start方法先于它的每一个动作
6:线程的所有操作先于线程的终结(Thread.join())
7:线程的中断(interrupt())先于被中断线程的代码
8:对象的构造函数执行结束先于finalize()方法
四 先行发生原则(happens-before)
先行发生原则避免了指令重排序带来的问题。(不是所有的指令都能重排序)比如如下:
写后读 a = 1;b = a; | 写一个变量之后,再读这个位置 |
写后写 a = 1;a = 2; | 写一个变量之后,再写这个变量 |
读后写 a = b;b = 1; | 读一个变量之后,再写这个变量 |
以上语句不可重排,但是a=1;b=2;是可以重排的