主内存与工作内存
Java内存模型规定了所有的变量都存储在主内存中,而每条线程有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本拷贝,线程主要是对自己对应的工作内存中的变量操作,不能直接操作主内存。
![](https://img.haomeiwen.com/i5460809/02db0324a99f4e2f.jpg)
内存间交互
- 主内存和工作内存交互的操作:
① lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
② unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
③ read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
④ load(载入):作用与工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
⑤ use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。
⑥ assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
⑦ store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
⑧ write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。 - 主内存和工作内存交互的规则:
① 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取,工作内存却不接收这种情况,反之也不允许出现,就是read+load或者store+write必须组合出现。
② 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化的同步回主内存。
③ 不允许一个线程无原因地把数据从线程的工作内存同步回主内存中。
④ 一个新的变量只能从主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。
⑤ 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
⑥ 如果对同一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
⑦ 如果一个变量事先没有被lock操作锁定,那就不允许对它进行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
⑧ 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store+write操作)·。
重排序
- 什么是重排序
指编译器或者处理器为了优化程序性能而对执行序列进行重新排列顺序。 - 重排序的核心问题:数据依赖问题
什么是数据依赖?
概念:只要重排序两个操作的执行顺序,对于最终的结果会发生不同变化的结果,就说明两个操作具有数据依赖问题。 - 重排序举例
假如我们要执行两步操作,读read和写write某个值value = 1。
初始化: value = 1
情况1 ,先读后写:结果:value = 3; value2 = 1
value2 = value;
value= 3;
情况2,先写后读:结果:value = 2;value2 = 2
value = 2;
value2 = value;
说明:由于例子中操作的执行顺序的改变,导致了最终结果的不同,而单个处理器中执行的指令序列和单个线程中执行的操作会严格遵守数据依赖性的问题。(不同的处理器或者不同线程之间数据依赖性不会被考虑的)。
- 单线程下的重排序
as-if-serial语义:
针对上面的数据依赖性问题,提出了as-if-serial语义。语义就是不管你怎么重排序,单线程的程序执行结果是永远不会变。编译器和处理器为了遵守这种的语义规则,就不会对存在数据依赖性的操作进行重排序了。
as-if-serial语义的好处:
对于单线程程序,就无需担心程序会因为重排序这种优化,导致结果不同的效果。同时也就无需担心单线程对于内存中数据。jmm提供的这种语义,就会起到一个内存屏障的作用防止重排序带来的问题。同时这种语义保证了单线程的方式下,值不会因为重排序发生变化,保证了内存可见性的问题。 - 多线程下的重排序
对于多线程程序,as-if-serial语义就不能保证什么了。
举例:
初始化:b = true;a = 1
线程1:b= false
线程2: if(b) a = 2
情况1:线程1先执行,线程2后执行。
结果:读到的a为1
情况2:线程2先执行,线程1后执行
结果:读到的a为2 - 重排序总结
排序是为了优化程序,对于单线程程序是可以保证防止数据的依赖性问题,但是对于多线程程序,重排序反而却产生了数据的依赖性。所以多线程程序中我们不能依赖于as-if-serial语义了。
顺序一致性
- 什么是顺序一致性
顺序一致性就是存在的多个线程,每一个线程都按照程序的顺序进行内存的读和写的操作,每一个线程都是按顺序执行,不存在竞争资源的特点。 - 顺序一致性的特点
① 一个线程的所有操作都必须按照程序的顺序执行。
② 所有的程序都只能看到一个单一的操作执行顺序,并且每个操作都必须原子执行并且还得对所有线程可见。
所以说顺序一致性内存模型是一个理想的理论参考模型! - JMM与顺序一致性
没有采用同步的程序在JMM中执行时,是根本不会遵循顺序一致性的,所以程序的执行顺序无序,多线程的执行操作也是无序的。
① 顺序一致性这种理想模型对于单线程会按照程序顺序执行,但是JMM单线程中不会单纯的按照程序顺序执行(重排序你懂的)。
② 顺序一致性理想模型会保证在多线程情况下,会看到一致的操作结果。但是JMM不保证所有线程看到一致的操作结果。 - JMM中采用Synchronized关键字实现顺序一致性
举例:
初始化: a = false; b =1
synchronized void method1() {
b = 2;
a = true;
}
synchronized void method2(){
if(a) {
b = 3;
}
}
解释:我们都知道加了关键字synchronized关键字,线程1将method1执行完,线程2才会执行method2。对于顺序一致性模型中,所有操作都是按照程序顺序执行。但是在每个方法内,JMM会进行没有数据依赖性的重排序。但是JMM是不允许临界区(方法内的代码)逸出到临界区外。如果逸出将会破坏监视器语义。所以未采用synchronized同步的程序在JMM中整体的执行顺序是无序,对于多个线程的操作顺序同样也是无序的。
happens-before
- happens-before的定义
① 如果操作1happens-before操作2,那么操作1执行结果对于操作2是可见的。操作1的执行顺序在操作2前面。
② 两个操作之间存在happens-before关系,只要不改变程序的结果就可以。 - happens-before规则
① 程序顺序规则:一个线程中的操作,会happens-before于该线程后续操作。
② 监视器锁规则:对于一个锁的解锁happens-before于随后对这个锁加锁。
③ volatile修饰变量的规则:对于一个volatile修饰的变量得写happens-before于任意对这个变量的读。
④ 传递性:A happens-before B ; B happens-before C 那么A happens-before C
⑤ start规则:如果线程A执行了start方法启动线程B,那么线程A执行start启动线程B,happens-before于线程B的任意操作。
⑥ join规则:线程A执行join方法等待线程B终止,那么线程B的任意操作都happens-before于线程A直到线程B执行完毕返回。
总结
- 线程间的通信策略:使用每个线程对应的工作内存与主内存交换数据。
- 使用重排序的意义是为了优化程序性能,java内存模型保证单线程下的重排序执行结果不会发生改变;但是多线程重排序执行结果不会保证。
- 使用顺序一致性的意义是为了防止资源竞争,让多线程按程序顺序执行,JMM本身根本不遵循,除非使用synchronized关键字控制。
- happens-before原则保证先行发生的结果对后面的操作可见并产生影响。
网友评论