我们常说的JVM内存模式指的是JVM的内存分区;而Java内存模式是一种虚拟机规范。
Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
硬件内存架构
-
计算机的组成
电脑中,CPU的运行计算速度是非常快的,而其他硬件比如IO,网络、内存读取等等,跟cpu的速度比起来是差几个数量级的。而不管任何操作,几乎是不可能都在cpu中完成而不借助于任何其他硬件操作。所以协调cpu和各个硬件之间的速度差异是非常重要的,要不然cpu就一直在等待,浪费资源。而在多核中,不仅面临如上问题,还有如果多个核用到了同一个数据,如何保证数据的一致性、正确性等问题,也是必须要解决的。目前基于高速缓存的存储交互很好的解决了cpu和内存等其他硬件之间的速度矛盾
-
CPU缓存架构
CPU缓存即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很高的存储器。由于CPU的速度远高于主内存,CPU直接从内存中存取数据要等待一定时间周期,Cache中保存着CPU刚用过或循环使用的一部分数据,当CPU再次使用该部分数据时可从Cache中直接调用,减少CPU的等待时间,提高了系统的效率。
-
多CPU多核架构
缓存一致性问题:在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢? -
早期的总线锁
因为CPU 都是通过总线来读取主存中的数据,因此对总线加Lock# 锁的话,其他CPU 访问主存就被阻塞了,这样防止了对共享变量的竞争。但是锁总线对CPU的性能损耗非常大,把多核CPU 并行的优势直接给干没了!
-
MESI
当CPU写数据时,如果写的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态;
当CPU读取共享变量时,发现自己缓存的该变量的缓存行是无效的,那么它就会从内存中重新读取。
缓存中数据都是以缓存行(Cache Line)为单位存储;MESI 各个状态描述如下表所示:
-
伪共享
Cache Line大小是64Byte。
如果多个核的线程在操作同一个缓存行中的不同变量数据,那么就会出现频繁的缓存失效,即使在代码层面看这两个线程操作的数据之间完全没有关系。这种不合理的资源竞争情况就是伪共享(False Sharing)
避免伪共享:
缓存行填充
使用 @sun.misc.Contended 注解(java8)
需要在 Jvm 启动时设置 -XX:-RestrictContended 才会生效
Java内存模型
JMM描述的是一种抽象的概念,一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性、有序性、可见性展开的
Sequential Consistency Memory Model:顺序一致性模型。这个模型定义了程序执行的顺序和代码执行的顺序是一致的。也就是说 如果两个线程,一个线程T1对共享变量A进行写操作,另外一个线程T2对A进行读操作。如果线程T1在时间上先于T2执行,那么T2就可以看见T1修改之后的值。
缺点:每次对共享变量的修改都要立刻同步回主内存,不能把变量保存到处理器寄存器或者处理器缓存中。在多处理器并发执行程序的时候,会严重的影响程序的性能。
Happens-Before Memory Model : 先行发生模型。如果有两个操作A和B存在A Happens-Before B,那么操作A对变量的修改对操作B来说是可见的。这个先行并不是代码执行时间上的先后关系,而是保证执行结果是顺序的。
遵循Happens-Before原则
Java线程内存模型(JMM)是建立在先行发生的内存模型之上的,并进行了增强。
-
JMM与硬件内存架构的关系
JMM模型跟CPU缓存模型结构类似,是基于CPU缓存模型建立起来的,JMM模型是标准化的,屏蔽掉了底层不同计算机的区别。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存(线程私有数据区域)和主内存(堆内存)之分,因为JMM只是一种抽象的概念,是一组规则,并不实际存在,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中。
-
Java内存模型内存交互操作
lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中 -
同步过程
把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但Java内存模型只要求上述8大操作(原子操作)必须按顺序执行,而没有保证必须是连续执行。
-
同步规则
1、不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
2、一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。
3、一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
4、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。
5、如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
6、对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作) -
JMM针对long和double型变量的特殊规则
Java内存模型要求lock、unlock、read、load、assign、use、store、write这八种操作都具有原子性,但是对64位的数据类型(long和double),在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性,这就是所谓的“long和double的非原子协定”