本节是JVM的第二篇文章,我们来看看容易与JVM内存结构混淆的概念:JVM内存模型。如果对内存结构不是很了解的小伙伴,可以先去看看: 漫游JVM(一):JVM内存结构
内存模型(Java Memory Model),是线程间通讯的机制,描述了JVM中【主内存】和【线程本地内存】的关系,在Java虚拟机设计的时候,就参考了计算机中的【内存】与【CPU的高速缓存】的交互,因此两者JVM的内存模型与计算机的十分相似。
1 JVM的内存模型
JMM抽象了【主内存】和【线程本地内存】的概念。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在【主内存】中,每个线程都有一个私有的【线程本地内存】,【线程本地内存】中存储了该线程以读/写共享变量的副本。
2 三大性质
在描述分析Java并发编程的时候,常常会提及到【原子性】、【可见性】和【有序性】作为切入点,我们在学习JMM的时候,同样也可以以此来切入。
2.1 原子性
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,而在JMM中,【主内存】和【线程本地内存】的交互,就是通过下面8个原子操作来实现的:
- read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
- lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
2.2 可见性
可见性是指当一个线程修改了工作内存的变量后,其他线程能够立即得知这个修改。
为方便描述,下面将主存中的变量A称为【主A】,而各自线程对此的备份为【线A1】和【线A2】。
两个线程不可见:
1. 【主A】的数据为10
2. 线程1和线程2访问主存获得【主A】的备份,分别为【线A1】和【线A2】
3. 线程1对【线A1】进行修改,例如赋值为15
4. 此时线程2中的【线A2】仍然是10
如何解决这个问题呢?
MESI(缓存一致性协议)
在java中,如果使用volatile关键字来修饰上述的变量A,就可以解决上述的可见性问题。其实其原理是使用了【MESI(缓存一致性协议)】,MESI要求某个线程修改了本地内存中的变量时,必须将变量立马写入主存,同时其他线程将其本地内存的副本标记为失效,再次使用时,需要重新到主存中加载最新的数据。
具体的情况,我们结合JMM再来说明一下当修饰了volatile关键字后的情况:
- 修饰volatile关键字后,开启MESI
- 当某个线程对本地内存中的变量[assign]时,会立即将数据[store]->[write],从而将数据立马写入到主存中
- 在写入的过程实际上还会加锁,即[loca]->[store]->[write]->[unlock],因为上了锁,其他线程如果想在再获取数据,只能等待写入完成且释放锁后,才能再次读取到主存的数据。
- 其他线程都会因开启MESI而持续监听总线,当监听到其他线程有数据即将写入(到达总线),则会将自己的本地内存中的副本标记为失效,如果仍需使用这份数据时,只能再次到主存中重新读取一份最新的数据。
以上,就是通过添加volatile关键字来解决可见性的问题,此外,volatile关键字除了解决可见性问题,还解决了有序性的问题,下面再来看看有序性。
2.3 有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
为什么需要重排序呢? 主要是为了在不改变程序语义的前提下,尽可能的减少寄存器的读取、存储次数,充分复用寄存器的存储值。如:
① int a = 1;
② int b = 2;
③ int c = a+1;
- 假设编译器没有重排序,那么①完成后需要与寄存器交互一次(存入),而③完成后又要与寄存器交互一次(取出)。
- 如果编译器使用了重排序,执行的顺序会变成①③②,在①完成后,先不存入主存,而是让③完成处理后,再存入主存,这样就减少了一次取出操作了。
这样的重排序在单线程的情况下并不会出问题,因为其最终结果都是正确的,但在多线程的环境下,这种重排序就可能导致线程不安全。如另一个线程需要读取a时,a因重排序而先不存入主存,此线程读取的数据已经不是最新值。
在Java里面,可以通过volatile关键字来通知编译器禁止对此变量进行重排序。
happens-before原则
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
1. 程序次序规则:
在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变。
2. 管程锁定规则:
就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
3. volatile变量规则:
就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
4. 线程启动规则:
在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
5. 线程终止规则:
在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()规则。
6. 线程中断规则:
对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
7. 传递性规则:
这个简单的,就是happens-before原则具有传递性,即hb(A, B) , hb(B, C),那么hb(A, C)。
8. 对象终结规则:
这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
3 小结
本节的主要内容是围绕JVM的内存模型:
- JVM的内存模型
- 主存
- 线程工作线程
- 三大性质
- 原子性
- 8个原子操作
- 可见性
- MESI(缓存一致性协议)
- 有序性
- happens-before原则
- 原子性