浅析java内存模型---JMM模型、顺序一致性模型、volatile内存语义、内存屏障

目录

JMM是什么

JMM与硬件内存的关系

内存交互操作

变量进行使用、修改的过程

 JMM的内存可见性保证

顺序一致性模型

volatile内存语义

volatile的特性

volatile写和读的内存语义

volatile可见性实现原理

JMM方面volatile的可见性实现

硬件方面volatile的可见性实现

volatile在Hotspot的实现

字节码解释器实现

模板解释器实现

lock前缀指令

指令重排序

volatile的重排序规则

内存屏障

内存屏障是什么

内存屏障的作用

volatile JMM内存屏障插入策略

 JVM内存屏障

硬件内存屏障


哈喽各位大佬,本次,我们来浅析一下java内存模型,也就是 JMM模型,当然,还会解释一下顺序一致性模型相关的,以及volatile内存语义相关的东西,还有一部分内存屏障,请各位大佬坐好~

在开始前,我们要先知道一个问题,JMM模型,是什么

JMM是什么

JMM,全称 java memory model,java内存模型,它用于屏蔽掉各种硬件和底层操作系统的访问差异性,让java程序在各个平台下都能实现相同的并发效果,JMM规范了java虚拟机和计算机内存如何协同工作,规定了一个线程如何、何时可以看到由其他线程修改过的共享变量的值,以及在必须的时候,如何同步访问的共享变量,JMM是围绕原子性、有序性、可见性来进行展开的

JMM模型,是一个共享内存模型,与线程间通信有关

JMM与硬件内存的关系

JMM内存模型,与硬件内存之间是有很明显差异的,硬件内存并不会分所谓的线程堆栈

内存交互操作

对于主内存与工作内存之间的交互协议,也就是说,如何把一个变量从主内存拷贝到工作内存,如何把一个工作内存同步到主内存,这中间的实现细节,java内存模型规定了以下几种操作

lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态

unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中

use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作

assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作

store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作

write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中

如图所示

 JMM还规定了,在执行上述的操作时,必须满足一些规则

如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行

不允许read和load、store和write操作之一单独出现

不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中

不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中

一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作

一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现

如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值

如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量

对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)

变量进行使用、修改的过程

让我们来看一幅图

线程A和线程B都要使用一个变量,他们都会先读取自己本地内存,栈内的副本变量,读不到,才会去读主内存(副本当然会存在失效问题,无论是被动,还是主动)

线程A和线程B都要使用一个变量,假设,线程A执行变更完,它并不会直接把结果给线程B,而是写到主内存,线程2从主内存获取,放在自己栈内存中的一个副本,当然,栈内存,堆内存,都是逻辑上的,非物理上的

我们再来看一副图,了解变量进行使用的过程,以及进行修改的过程---我写字丑,大哥大姐别骂了

 JMM的内存可见性保证

按照程序的类型,java程序的内存可见性可以分为以下几类

1、单线程程序

2、正确同步的多线程程序

3、没有同步或没有正确同步的多线程程序

首先,第一点,单线程程序,单线程程序不会出现内存可见性的问题,因为编译器,处理器等, 会保证单线程程序的执行结果在顺序一致性模型中执行的结果一致

第二点,正确同步的多线程程序,它们的执行具有顺序一致性,也就是说,程序的执行结果,和在顺序一致性模型中执行结果是一致的,这是JMM关注的重点,JMM通过限制编译器和处理器的重排序来为我们提供可见性的保证

第三点,没有同步或没有正确同步的多线程程序,JMM只能为它们提供最小的安全保证,线程执行时读取到的值,要么,是之前某个线程写入的,要么,是默认值未同步到JMM中的执行时,整体上来说,是无序的,所以,其运行结果无法知晓,JMM无法保证其程序执行结果与其在顺序一致性模型中的执行结果一致

未同步的程序,在顺序一致性模型和JMM模型中的执行特性,有这些差异

顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行,比如正确同步的多线程程序在临界区内的重排序。

顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。

顺序一致性模型保证对所有的内存读/写操作都具有原子性,而JMM不保证对64位的long型和double型变量的写操作具有原子性(32位处理器)。

顺序一致性模型

上面的可能有朋友们没理解,那我补充一下,什么是顺序一致性模型

顺序一致性模型,是一个理想化的理论参考模型,它有极强的内存可见性保证,顺序一致性模型,有以下特性:

1、一个线程中的所有操作必须按照程序的顺序来执行

2、无论程序是否同步,所有线程都只能看到一个单一的操作执行顺序

3、在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见

通俗来说,顺序一致性模型,它有一个单一的全局内存,这个全局内存可以供任何线程使用,同时,每个线程必须按照程序的顺序来执行读写操作,当多个线程同时并发执行时,所有线程对内存的读写都会被串行化

举个栗子,呸,例子,抱歉我实在是太饿了.......

假设有A和B两个线程,它们在程序里的操作顺序都是1-2-3

也就是说,A线程操作顺序是A1→A2→A3

B线程操作顺序是B1→B2→B3

如果A和B这两个线程,我们使用某种方式,让他们正确进行同步,A线程执行完,B线程执行,那么在顺序一致性模型,操作效果将是

A1 → A2 → A3 → B1 → B2 → B3

线程A执行顺序不变,线程B执行顺序不变

假设,A和B这两个线程,没有同步,那操作效果可能是

B3 → A2 → B1 → A3 → B2 → A1

虽然,执行顺序是无序的,但,注意!所有线程,即A和B,他们将看到一个一致的执行顺序,也就是说,A和B看到的,都是上面这个执行顺序结果

相同的没有同步,放到JMM里,那就不好说A和B线程大家看到的是什么结果了

看到这里,是不是明白了上面JMM模型和顺序一致性模型的区别

volatile内存语义

既然,有可见性的问题,那我们就解决它,我们可以使用关键字 volatile

volatile的特性

volatile对于三大特性的说明

可见性:对于一个 volatile 变量,总是能够看到任意线程对这个变量的最后写入

原子性:对于任意的单个 volatile 变量的读写是有原子性的,但注意!volatile++这种复合操作,没有原子性

有序性:对 volatile 修饰的变量读写操作前后,添加了各种内存屏障,来进行指令重排序,以此保证有序性

volatile写和读的内存语义

读:读一个被volatile修饰的变量时,JMM会把此线程对应的本地内存的副本变量置为无效,线程接下来将会从主内存读取共享变量

写:写一个被volatile修饰的变量时,JMM会把此线程对应的本地内存中的共享变量值刷新到主内存

volatile可见性实现原理

volatile可见性原理的实现,包括两方面,一方面是从JMM,一方面是从硬件

JMM方面volatile的可见性实现

volatile修饰的变量,其read、load、use操作和assign、store、write操作,必须是连续的,也就是说,修改后必须刷新到主内存,使用时,必须从主内存刷新

硬件方面volatile的可见性实现

硬件方面,volatile关键字,通过 lock 前缀指令,锁定变量缓存行区域,并写回主内存,这个动作也被称为缓存锁定,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,一个处理器的缓存写到内存,会导致其他处理器的缓存全部失效

volatile在Hotspot的实现

首先,要知道Hotspot是什么,Hotspot就是虚拟机

在Hotspot中,volatile分为两种实现,字节码解释器实现和模板解释器实现

但最后,其实都是调用了lock前缀指令

字节码解释器实现

JVM中的字节码解释器,就是用C++实现了JVM指令,优点是相对简单,容易理解,但,执行慢

字节码解释器在判断是 volatile 修饰后,会执行

OrderAccess::Storeload();

在Linux X86 架构中,

OrderAccess::Storeload(); 调用了

fence();


inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }

可以看到,fence这个函数方法,会执行汇编命令 lock

模板解释器实现

模板解释器,就是为每个指令都书写了一段汇编代码,启动的时候,将每个指令和对应的汇编代码绑定, 某种程度上来说,将效率提升到了最大

可以看到,使用了Assembler::StoreLoad和Assembler::StoreStore,通常,在X86架构下,生效的只有前者

而前者Assembler::StoreLoad,在最后还是会调用lock前缀指令

lock前缀指令

lock前缀指令,不是内存屏障, 但它有内存屏障的效果,它的作用是会等待它之前的所有指令完成,并将缓存中的写操作写到内存,根据缓存一致性协议,缓存的副本将失效,并且禁止该指令与其前后的读写指令进行重排序

指令重排序

只要程序的最终结果与它顺序化情况的结果相同,那么指令的执行顺序可以与代码不一致,这个过程就叫指令重排序

当然,指令重排序有它的意义,JVM可以根据处理器的特性,适当的对机器指令重排序,让机器指令更能符合CPU的执行逻辑和特性,最大限度发挥机器的性能,并且,在编译器和CPU中,都能执行指令重排优化

源代码 → 编译器优化重排序 → 指令并行重排序 → 内存系统重排序 → 执行的指令序列

volatile的重排序规则

内存屏障

认知一个很重要的东西,内存屏障

内存屏障是什么

内存屏障,其实就是一个栅栏,使CPU或者编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作

内存屏障的作用

内存屏障十分重要,为了提高性能,很多时候都是乱序执行的,但,不能任何时候都乱序,内存屏障有两个作用:

1、阻止屏障两边的指令重排序

2、刷新处理器缓存/冲刷处理器缓存

对于 load 来说,在读指令前插入内存屏障,可以让高速缓存中的副本失效,从而重新从主内存加载,对于 store 来说,在写指令后插入内存屏障,可以让写入缓存的数据立刻写回主内存

像上面提到的 lock 前缀指令,就实现了类似的能力,它先对总线和缓存加锁,然后执行后面的命令,最后释放锁后会把高速缓存中的数据刷新回主内存

在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放

当然,不同的硬件实现内存屏障的方式不同,java内存模型就帮我们屏蔽了这种底层的差异,由JVM来为不同的硬件生成对应的机器码

volatile JMM内存屏障插入策略

1. 在每个volatile写操作的前面插入一个StoreStore屏障

2. 在每个volatile写操作的后面插入一个StoreLoad屏障

3. 在每个volatile读操作的后面插入一个LoadLoad屏障

4. 在每个volatile读操作的后面插入一个LoadStore屏障

 JVM内存屏障

LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

X86架构只有store load可能会重排序,所以只有StoreLoad屏障对应它的lock前缀指令,其他屏障对应空操作

硬件内存屏障

硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel)来提供一致性的能力

X86架构主要有这几种内存屏障

1. lfence,是一种Load Barrier 读屏障

2. sfence, 是一种Store Barrier 写屏障

3. mfence, 是一种全能型的屏障,具备lfence和sfence的能力

4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能;Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁

OK,本次的整理总结就到此为止

这块是一个很大的块,需要慢慢看,慢慢理解

嘛,祝各位晚安~

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值