java内存模型
java的内存模型以及线程主要是为了顺应当前的多任务处理器,随着硬件的不断升级,如果现在还是只能单线程串行执行的话,将会浪费处理器的大量时钟,cpu的执行速度非常快,导致了和等待资源之间有时间差,在等待阶段,cpu处在空闲时间,为了有效利用这段空闲时间,就可以引入多线程处理。注意是有效利用,多线程是一把双刃剑,因为多线程程序,需要额外的一些资源,比如阻塞/唤醒等操作需要操作系统在用户态与内核态进行转换,而这种转换需要耗费比较大的资源,如果系统存在大量的阻塞/唤醒操作,那么会浪费大量的cpu时钟周期,导致系统吞吐量降低,合适的线程数量可以充分利用cpu的空闲时间,从而提升系统的吞吐量。随着多线程的引入,那么将会带来访问共享资源的有效性问题,java内存模型便是为了解决多线程访问共享资源保证该资源的有效性,线程则是为了有效的利用cpu空闲时钟,同时引入了如何调度才能更加有效的问题。
java内存模型可以类比硬件的内存模型。cpu处理速度非常快,而从物理内存读取和写入数据非常慢,所以引入二级缓存,cpu从二级缓存读取或写入数据,然后二级缓存会读取或写入到物理内存中,这样cpu就可以在等待IO资源的时候去处理其他的事情。二级缓存与物理内存的数据同步只需要遵循一些相关的协议便可以。同样,java的内存模型可以在硬件的内存模型中找到一些影子。java内存模型由工作内存,主内存以及这两者之间的一系列操作组成。主内存存在于堆中,而工作内存存在于线程的局部变量表中,这两者之间的数据交换遵循一系列的操作,这些操作由java内存模型定义,分别为:
操作 | 名称 | 作用 |
lock | 锁定 | 作用于主内存变量,它把一个变量标识为一条线程独占使用状态 |
unlock | 解锁 | 作用于主内存变量,他把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 |
read | 读取 | 作用于工作内存变量,从主内存读取一个变量的值,并且传递到线程的工作内存中,供之后的load操作使用 |
load | 载入 | 作用于工作内存变量,把read操作从主内存中获取的变量值赋给工作内存的变量的副本中 |
use | 使用 | 作用于工作内存变量,当虚拟机需要遇到需要使用变量值的字节码指令时把工作内存的变量副本的值传递给执行引擎 |
assign | 赋值 | 作用于工作内存变量,把从执行引擎收到的值赋给工作内存的变量赋值 |
store | 存储 | 作用于工作内存变量,把工作内存变量的值传递到主内存 |
write | 写入 | 作用于主内存变量,把store操作传递到主内存的变量的值赋给工作内存的变量中 |
以上这8中操作必须满足以下规则:
1.不允许read和load,store和write单独出现 |
2.不允许一个线程丢弃最近的assign操作,即变量在工作内存中改变了以后必须同步回主内存 |
3.不允许一个线程无原因(没有经过assign操作)的把数据从线程的工作内存同步回主内存 |
4.一个新的变量只能在主内存中创建,不允许在工作内存中直接使用一个未被初始化(load/assign)的变量 |
5.一个变量只允许一个线程对其进行lock操作,但是可以被同一个线程执行多次 |
6.如果对一个变量执行lock操作,将会清空工作内存此变量的值,在执行引擎中使用这个变量之前,需要重新执行load/assign操作初始化此变量 |
7.如果一个变量之前没有进行lock操作,那么不允许对齐进行unlock操作,也不允许unlock一个被其他线程锁定的变量 |
8.一个变量执行unlock之前,必须先把此变量的值同步回主内存中,执行store/write操作 |
java内存模型保证共享的资源在多线程下面可以串行执行,不同线程获得到的共享资源都是有效的。
java中的volatile关键字定义的变量需要满足更进一步的规则:
1.load,read,use操作必须同时出现 |
2.assign,store,write必须同时出现 |
3.使得volatile修饰的变量不会被指令重排序,保证代码的执行顺序与程序的顺序相同 |
volatile关键字第三点需要解释一下:假设动作A是线程T对变量V实施的use或assign动作,假定动作F是与动作A对应的load或store操作,假定动作P是与F对应的read或write操作;假设动作B是线程T对变量W实施的use或assign动作,假定动作G是与动作B对应的load或store操作,假定动作Q是与G对应的read或write操作;如果A先于B,那么P先于Q。
java内存模型围绕着原子性,可见性,有序性来建立的。java中的基本数据类型都是原子的,但是64位JDK中的long和double分成2个32位的操作来进行。可见性是指一个线程改变了共享变量的值以后,会同步回主内存,然后其他线程就可以看见这个变量的最新值。有序性可以总结为:在本线程内观察,所有操作都是有序的;在一个线程中观察另一个线程,所有操作都是无序的。
在java中,除了volatile以及synchronized来保证有序性外,还有一些“先行发生(happen-before)”原则,具体如下:
1.程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作(根据控制流顺序) |
2.管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作 |
3.volatile规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作 |
4.线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作 |
5.线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检测到线程已经终止执行。 |
6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。意思就是只有在线程还没有中断之前才能调用interrupt方法 |
7.对象终结规则:一个对象的初始化完成先行发生于它的finalize方法的开始 |
8.传递性:如果A操作先行发生于B操作,B操作先行发生于C操作,那么A操作先行发生于C操作 |
java线程的实现方式是用户线程与内核线程的混合实现。使用内核线程有利于阻塞唤醒等操作,用户线程在创建和销毁代价比较小,所以两者组合起来实现比较灵活。具体可以参见《深入理解java虚拟机》Page334.
爱情终将消失于茫茫的时间洪流之中,沉淀于厚重的黄泥沙丘之下...