JVM 的内存模型与线程
1.性能何处寻
计算机的CPU比起其他所有的设备,都快得多,所以怎样尽量复用 CPU 的时间片,是压榨计算机性能的目标。多核和并发,使得阿姆达尔定律大显神威,超越摩尔定律成为提升系统性能的金科玉律 - 现在单核计算能力已经无法垂直提升,要水平提升核数来提升整体性能。
2.缓存一致性问题(Cache Coherence)
软件缓存,不过是硬件缓存的模仿,真正的缓存,早已存在于计算机的多级存储体系结构中。JVM 里,我们可以认为每个处理器都会在主内存(Main Memory)之外有高速缓存作为工作内存(Working memory)。除此之外,处理器和 JVM 都可能出现指令重排(Instruction Reorder)的的情况。工作内存是线程 Save 和 Load 的主要场所,主内存则是他们沟通的场所。
3.JVM 的对象信息
Java Object 除了基本的内存轮廓以外,还有:
- Mark Word(对象的 Hash Code 的缓存值、GC标志、GC年龄、同步锁等信息)。
- Klass Point(指向对象元数据信息的指针,指向 .class 的指针吗?不是,是指向方法区的类型元数据的指针。.Class文件实际上是那个区域的另一个入口了。)。
- padding。如果对象是8位对齐的(也就是最长标量类型对齐的),则不存在padding。
4.内存间(主内存与工作内存)相互操作
Java内存模型(Java Memory Model)定义了八种内存操作(而不是字节码)。虚拟机在是现实必须保证每一种操作都是原子的、不可再分的(对于 double 和 long 类型的变量来说,load、store、read 和 write 操作在某些平台上可以例外):
- lock 把主内存变量为一个线程锁定起来。
- unlock 把主内存的变量解锁,这样其他线程才能锁定。
- read 把一个变量的值,从主内存读到工作内存里。是 load 指令的前置动作。
- load 把read出来的变量,放到工作内存的副本里。
- use 把工作内存的值传给工作执行引擎。
- assign 把执行引擎里得到的值传给工作内存的变量副本。它是一种工作内存的局部写。
- store 把工作内存中的变量的值传递给主内存。
实际上的执行顺序恐怕是 read->load->use->assign->store-> write。
如果要把一个变量从主内存复制到工作内存,那就要按顺序地执行 read 和load 操作,如果要把变量从工作内存同不会主内存,就要执行 store 和 write 操作。 JMM 只要求上述两类操作必须按顺序执行,没有保证必须是连续执行,也就是说在 read 和 load之间、store 和 write 之间是可插入其他指令的。如对主内存的变量 a、b 进行访问的时候,可能出现 read a、read b、load b、load a 的操作顺序。
除此之外, JVM 还规定了额外的指令执行的偏序规则(正好也有八条):
- 不允许 read 和 load、store 和 write 操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起了写回但工作内存不接受的情况。
- 不允许一个线程丢弃它的最近的 assign 操作,即变量在工作内存中发生了改变必须(最终)把该变化同步回主内存里去。
- 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load 或者 assign)的变量,换句话说就是对一个变量实施 use 和 store操作之前,必须经过 assign 和 load 的操作。
- 一个变量在同一个时刻只允许一条线程对它进行 lock 操作,且 lock 操作可以被同一个线程执行多次(多种可重入锁的底层机制就在这里了)。而且只有执行相同数量的 unlock 操作,才能彻底解锁该变量。
- 如果对一个变量进行 lock 操作,会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 和 assign 操作。也就是说,这是一个 flush 加上 reload的过程。
- 如果一个变量没有被 lock 锁住,则 unlock 非法,只有本线程才能unlock。
- 对一个变量进行unlock操作之前,必须先把变量同步回主内存中(执行 store 和 write 操作)。也就是说,变量被线程锁住以后,不是在主内存上工作,而是在自己的工作内存里被使用的,这也印证了上面的八种指令中的 use 必须在 load 之后工作,执行引擎必须使用 use 的印象。
5.volatile关键字
volatile 关键字具有可见性,会使得每次写操作,都会导致全flush 的出现(assign必然导致 store 和 write 回主内存),读操作必须read + load至工作内存, use 到执行引擎(而不能只是use上次留在工作内存里的值),必然总是得到最新的值,不管中间是否有不一致的暂时情况发生,读的语义必然是一致正确的。而如果没有这条语义,use得到的值,可能是之前 use 和 assign 得到的值。
注意,如果使用字节码分析多线程操作,即使只出现一条指令,也不能认为实际执行的机器指令是原子化的,但如果出现多条字节码指令,那么必然操作没有原子性。这也是 volatile 修饰的变量只是轻量级同步,不能做到真正互斥原子化的原因。它只保证了可见性。
因此,只有两种情况,不必然要使用标准同步机制:
- 远算结果不依赖指定非栈上变量的当前值,或者能够确保单线程修改指定变量的当前值。
- 变量不需要与其他变量参与同一个不变性约束。
此外,volatile关键字还可以通过插入内存屏障(memory barier)阻止内存指令重排(instruction reorder),阻止特定的赋值顺序被打乱。这点在 Java 5以前是做不到的,也就会经常性导致 Double Check Lock 在 Java 5以前失败。具体地说,相关联的操作是不可重排序的。相关联的read->load->use/assign->store->write可以看做是不可被重排插入中间指令的,一个指令 read 先于另一个指令 read,那么所有相关联的指令都是前者先于后者。这被称为“线程内表现为穿行语义”(Within-Thread As-If-Serial Semantics)。
6.Java内存模型的(Java)的特性
6.1 原子性(Atomicity)
8个操作,read、load、use、assign、store、write这六个操作是必须原子的(64字节的 long、double 非原子性是可以由lock 和 unlock 的更强原子语义包裹起来规避掉的)。lock 和 unlock 操作虽然不是字节码,但几乎同意的 monitoerenter和monitorexit却是字节码指令。
6.2 可见性(Visibility)
一个线程的修改,立刻可以被另一个线程看到,方法主要有三个:
- 同步块
- final (final 并不是不可更改的,所以依然有工作内存修改后flush的问题)
- volatile
6.3 有序性(Ordering)
volatile和同步块可以保证这点。方法内的指令不会被重排,是一个特别重要的不会产生特别副作用的保证。
6.4 volatile 和同步块比较
volatile不具有原子性,其他场景volatile和同步块都可以使用。
6.5 先行发生原则(happens-before)
JVM 为程序中所有的操作定义了一个偏序关系(偏序关系 π 是集合上的一种关系,据有反对称、自反和传递属性。但对于任意两个元素x,y来说,并不需要一定满足 x π y, y π x的关系。我们每天都在使用偏序关系表达喜好。),称之为 Happens-Before。只有操作 A 和操作 B 之间满足 Happens-Before 关系,才能保证
保证操作 B 一定能够看到操作 A 的结果。
Happens-Before 的八条原则包括:
- 程序顺序原则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作线性发生于书写在后面的操作。这一条并不绝对,首先要考虑控制流循环跳转的问题,其次是,如果后操作无法感知前操作(即不存在依赖关系),则指令重排仍然可能发生。
- 监视器锁定原则(Monitor Lock Rule):一个 unlock 操作时间顺序上先行发生于后面对同一个锁的 lock 操作。(单纯的lock 操作语义只提供了可见性,这条原则还保证了有序性。)
- volatile 变量原则(volatile variable rule):对 volatile 变量的写入操作,必须要在读取操作时间顺序之前进行。
- 线程启动规则(Thread Start Rule):Thread对象的 start()方法先行发生于此线程的每一个动作。
- 线程终止规则(Thread Termination Rule):线程中所有操作,都先行发生于线程的终止检测。常见终止检测是 Thread.join() 的返回,Thread.isAlive()的返回。
- 线程中断原则(Thread Interruption):对线程 interrupt() 方法的调用先行发生于被中断线程检测中断事件的发生。常见检测事件的方法是 Thread.interrupted()。
- 对象终结原则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。
- 传递性(Transitivity) 操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,操作 A 先行发生于操作 C。