JVM——一文搞懂JMM(Java虚拟机)

1、什么是JMM

JMM就是Java内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以Java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的并发效果。

Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行线程不能直接读写主内存中的变量

不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。

在这里插入图片描述

温馨提醒一下,这里有些人会把Java内存模型误解为**Java内存结构,然后答到堆,栈,GC垃圾回收,最后和面试官想问的问题相差甚远。实际上一般问到Java内存模型都是想问多线程,Java**并发相关的问题

2、JMM定义了什么

分别是:原子性,可见性,有序性。这三个特征可谓是整个Java并发的基础。

2.1、原子性

JMM只能保证基本的原子性,如果要保证一个代码块的原子性,提供了monitorentermoniterexit 两个字节码指令,也就是 synchronized关键字。因此在 synchronized 块之间的操作都是原子性的。

2.2、可见性

可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。可见性的实现方式包括volatilefinalsynchronized

  • volatile Java是利用volatile关键字来提供可见性的。 当变量被volatile修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。而普通变量则不能保证这一点;
  • synchronized synchronized的原理是,在执行完,进入unlock之前,必须将共享变量同步到主内存中;
  • final final修饰的字段,一旦初始化完成,如果没有对象逸出(指对象为初始化完成就可以被别的线程使用),那么对于其他线程都是可见的。

2.3、有序性

Java中,可以使用synchronized或者volatile保证多线程之间操作的有序性。实现原理有些区别:

  • happens-before 原则: happens-before原则是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,也就是说发生操作B之前,操作A产生的影响能被操作B观察到。这里的“影响”包括修改共享变量,方法调用。
  • synchronized 机制: synchronized能够保证有序性是因为synchronized可以保证同一时间只有一个线程访问代码块,而单线程环境下,JMM能够保证代码的串行语义;虽然使用synchronized的代码块,还可以发生指令重排序,但是synchronized可以保证只有一个线程执行,所以最后的结果还是正确的。
  • volatile 机制: volatile的底层是使用内存屏障(详细请参看内存屏障章节)来保障有序性的。写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后。读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。

多线程面临的两个问题线程之间的通信和线程之间的同步,这两个问题如果仔细分析,从结果的角度看线程之间的通信就是可见性问题,线程之间的同步就是原子性和有序性的问题。

3、八种内存交互操作

在这里插入图片描述

  • lock (锁定): 作用于主内存中的变量,把变量标识为线程独占的状态;
  • read (读取): 作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用;
  • load (加载): 作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
  • use (使用): 作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign (赋值): 作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
  • store (存储): 作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
  • write (写入): 作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
  • unlock (解锁): 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

JMM对8种内存交互操作制定的规则吧:

  • 不允许readloadstorewrite操作之一单独出现,也就是read操作后必须loadstore操作后必须write
  • 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存;
  • 不允许线程将没有assign的数据从工作内存同步到主内存;
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施usestore操作之前,必须经过loadassign操作;
  • 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁;
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新loadassign操作初始化变量的值;
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量;
  • 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。

4、禁止指令重排序

首先要讲一下as-if-serial语义,不管怎么重排序,(单线程)程序的执行结果不能被改变。

为了使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率,只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序

重排序的种类分为三种,分别是:编译器重排序,指令级并行的重排序,内存系统重排序。整个过程如下所示:
在这里插入图片描述

指令重排序在单线程是没有问题的,不会影响执行结果,而且还提高了性能。但是在多线程的环境下就不能保证一定不会影响执行结果了。

所以在多线程环境下,就需要禁止指令重排序

volatile关键字禁止指令重排序有两层意思:

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行;
  • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

5、禁止指令重排的原理是什么?

首先要讲一下内存屏障,内存屏障可以分为以下几类:

  • LoadLoad 屏障: 对于这样的语句Load1LoadLoadLoad2。在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore 屏障: 对于这样的语句Store1StoreStoreStore2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore 屏障: 对于这样的语句Load1LoadStoreStore2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad 屏障: 对于这样的语句Store1StoreLoadLoad2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

5.1、在每个volatile读操作后插入LoadLoad屏障,在读操作后插入LoadStore屏障。

在这里插入图片描述

5.2、在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个SotreLoad屏障。

在这里插入图片描述

  • 9
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值