Java内存模型(和堆栈等不是同一层次的划分)
什么叫Java内存模型?
现代计算机通过指令的重排序来提升计算机的性能,而没有限制条件的指令重排序会使得程序的行为不可预测,JMM就是通过一系列的操作规则限制指令重排序的方式使得指令重排序不会破坏JMM提供的可见性,同时JMM通过让JVM在适当的位置插入内存栅栏来屏蔽JMM与底层平台内存模型之间的差异。
背景知识:
*每秒处理事务数:衡量一个服务性能的高低好坏,每秒处理事务数是重要的衡量指标之一
*高速Cache:由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机都不得不加入一层读写速度尽可能的接近处理器运算速度的高速缓存来作为内存和处理器直接的缓冲
*缓存一致性协议:用于处理给高速缓存中数据一致性的问题
*处理器<--->高速缓存<--->缓存一致性协议<--->主内存
*如果存在一个计算任务依赖另外一个计算任务中间结果,那么其顺序性并不能依靠代码的先后来保证,这是数据的依赖性,指令重排优化要遵循数据的依赖性
*Java线程<--->工作内存<--->sava和store操作<--->主内存
ps:放图的目的就是要类比上面两张图!!!
正文:
对比上面两张图,我们把处理器和java线程做类比,高速缓存和工作内存做类比,主内存是同一个,那么我们java中有没有类似缓存一致性协议的协议来处理工作内存和主内存之间的实现细节呢?即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回到主内存的呢
答案肯定是有的,java内存模型中(和java内存区域中的堆,栈,方法区等不是同一个层次的划分)定义了8种操作来实现主内存和工作内存之间的交互协议,每一种操作都是原子性的!
java内存模型中的8种操作:
1)lock:锁定,作用于主内存变量,把一个变量标识为一条线程独占状态
2)unlock:解锁,作用于主内存变量,把一个处于lock状态的变量释放出来,释放后的变量才可以被其他线程锁定
3)read:读取,作用于主内存的变量,把一个变量从主内存传输到线程工作内存,以便随后的load使用
4)load:载入,作用于工作内存变量,它把read操作从主内存中得到的值放入工作内存的变量副本中
5)use:使用,作用于工作内存变量,把工作内存变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令就会执行这个操作
6)assign:赋值,作用于工作内存变量,把从执行引擎接收到的值赋给工作内存变量,每当虚拟机遇到一个需要给变量赋值的字节码指令就会执行这个操作
7)store:存储,作用于工作内存变量,把一个工作内存变量的值送到主内存中,以便后面的write操作使用
8)write:写入,作用于主内存变量,把store操作从工作内存得到的变量的值放入主内存变量中
以上8个操作都是原子操作!
要从主内存读取一个数据,必定要执行read和load,而且read要在load前面执行,只是要求了执行的顺序,却没有要求一定要连续执行,所以我们可以进行指令重排,那我们这8种操作怎么保证多线程环境下是安全的呢?所以我们java中还存在8种操作的操作规则
操作规则:
1)read在load前,store在write前,都不能单独出现,得成对,出现还得满足顺序
2)工作内存中的变量改变后必须同步到主内存
3)不允许一个线程无原因的(没有发生任何assign操作)把数据从线程的工作内存同步到主内存中
4)新的变量只能在主内存中诞生,不允许在工作内存中使用一个没有被初始化(load和assign)的变量,即对一个变量实施use和store操作之前必须先执行过了assign和load
5)一个变量在同一时刻只允许一个线程对其进行lock,但lock操作可以被同一条线程执行多次,执行多次lock后只有进行相同次数的unlock才能释放变量
6)如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作来初始化变量的值
7)如果一个变量没有被lock操作锁定,那么不允许对它进行unlock操作,也不允许去unlock一个被其他线程lock的变量
8)对一个变量执行unlock操作之前,必须把此变量同步回到主内存中(执行store,write)
分析:
这8种操作加上这8种操作规则,虽然可以保证一些内存操作是安全的,但是它实现起来非常的繁琐,不好实现,我们有一种代替这8个规则的方法:先行发生原则,满足先行发生原则则可以保证这些内存在多线程环境下是安全的,如果不满足先行发生原则的话,我们的补救措施就是volatile和synchorized
先行发生原则(判断数据是否存在竞争,线程是否安全的重要依据):
天然的先行发生关系,这些先行发生关系无需同步就已经存在了,满足这些先行发生关系的话,我们就不可以对他们进行随意的指令重排,得设置内存屏障,防止被重排
具体的原则:
1)程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作,准确的说,应该是控制流顺序,而不是程序代码顺序,因为要考虑分支循环等结构
2)管程锁定原则:一个unlock操作先行发生于后面对同一个锁的lock操作,这里必须强调是同一个锁,后面是指时间上的先后顺序
3)volatile变量规则:对一个volatile变量的写操作要先行发生于后面对这个变量的读操作,这里的后面同样是指时间上的先后顺序
4)线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作
5)线程终止原则:线程中的所有操作都优先发生于对此线程的终止检测,我们可以通过Thread.join方法,Thread.isAlive的返回值等手段检测到线程已经终止执行
6)线程中断规则:对线程interrupu方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted方法检测到是否有中断发生
7)对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize方法
8)传递性:如果操作A先行发生于操作B,操作B先行发生于操作A,那么可以得出操作A先行发生于操作C的结论
分析:
两个操作如果不满足先行发生原则,那么这两个操作在并发环境下就是不安全的,需要采用volatile或者synchorized或者lock使得线程安全,如果他们满足先行发生原则,那么这两个操作在多线程环境下肯定是线程安全的
(当然,volatile在java里面的运算的非原子性的,导致volatile变量的运算在并发下也一样是不安全的,但是单个volatile变量的读写具有原子性!!!)
volatile变量规则:
关键字volatile是JVM中最轻量的同步机制,volatile具有两种特性:
*保证变量的可见性:对一个volatile变量的读,总是能看到(任意线程)对这个1volatile变量最后的写入,这个新值对于其他线程来说是立即可见的
*屏蔽指令重排序:指令重排序是编译器和处理器为了高效对程序优化进行的手段,下文有详细分析:
volatile语义并不能保证变量的原子性,对任意单个volatile变量的读写具有原子性,但类似于i++,i--这种复合操作不具有原子性,因为自增运算包括读取i,i+1,重新赋值三个步骤,并不具备原子性
由于volatile只能保证变量的可见性和屏蔽指令重新排序,只有满足下面两条规则时,才能使用volatile来保证并发安全,否则就需要加锁(synchorized,lock,Atomic原子类)来保证并发中的原子性
*运算结果不存在数据依赖,或者只有单一的线程改变变量的值
*变量不需要与其他状态变量共同参与不变约束
因为需要在本地代码中插入许多内存屏蔽指令在屏蔽特定条件下重新排序,volatile变量的写操作比读操作慢一些,但是其性能开销比锁低很多