1 为什么要使用JMM
Java虚拟机可以运行在不同的操作系统上,因此在不同的硬件和不同的操作系统下,内存的访问逻辑稍有差别。在这种情况下,有可能你开发的程序在某个系统环境下运行良好,而且线程安全。但是在别的系统环境下就有可能出现各种各样的问题。那JMM就是为了屏蔽硬件和操作系统对内存访问的差异,让一套代码在任何地方运行都能够达到相同的结果。
2 内存如何使用
JMM将内存划分为主内存和工作内存两种。
2.1 那么为什么这么划分呢?
举个例子,我们在开发的时候,都是先将远程代码拉下来,在本地修改完了之后再上传到代码库。这样,一个项目组的开发人员就可以同时工作,不需要排队的去直接修改远程库里面的代码。
JVM在设计的时候同样考虑到线程每次读取和写入都直接操作主内存的话,对性能影响比较大。因此,JVM的解决方案是每条线程都拥有各自的工作内存,并且工作内存中的变量是主内存中的一份拷贝。
在实际运行过程中,线程对变量的读写操作直接在工作内存中进行,操作完成后再去刷新主内存中的数据。主内存的数据发生变化后会通知其他线程,但通知的时机是不可控的,也不是我们可以控制的。
2.2 八种内存的交互操作
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
我们可以看到,这八种交互操作其实主要是围绕变量在主内存、工作内存、执行引擎中的存储和使用。
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write。
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存。
- 不允许一个线程将没有assign的数据从工作内存同步回主内存。
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量进行use、store操作之前,必须经过assign和load操作。
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁。
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存。
2.3 三种模型特征
一、原子性
原子性操作就是不能再继续拆分的单元。比如,a=1
是原子性的,a=a+1
是非原子性的。
另外,被synchronize关键字或其他锁包裹的操作也可以是认为是原子性的。
synchronized(this){
x = 1;
y = 1;
}
如果观察某个线程执行上面的代码,那么看到的结果是要么xy都被复制成功,那么xy都没有被赋值。
二、可见性
由于每个线程都有自己的工作内存,所以,当某个线程修改完变量后,在其他线程中未必可以看到这个变量已经被修改。
这时,有三种方式可以解决可见性的问题。
- volatile关键字修饰的变量被修改后会被立即更新到主内存中,而且每次使用的时候必须要从主内存中读取。
- synchronized保证unlock之前必须先把变量刷新回主内存。
- final修饰的变量在构造器中一旦完成初始化,其他线程就能看到该变量的值。
三、有序性
由于JMM的工作内存和主内存之间存在延迟,而且java会对一些指令进行重新排序,所以,在线程外部观察的话,所有操作都是无序的。volatile和synchronized可以保证程序的有序性,可以保证指令不进行重排序。