Jmm之Java内存模型

JMM是什么?

    JMM 就是 Java 内存模型(java memory model)。JMM 是一个抽象的概念,并不像 JVM 内存结构一样真实存在。它描述的是和多线程相关的一组规范,需要各个 JVM 的实现来遵守 JMM 规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。这样一来,即便同一个程序在不同的虚拟机上运行,得到的程序结果也是一致的。

    如果没有 JMM 内存模型来规范,那么很可能在经过了不同 JVM 的“翻译”之后,导致在不同的虚拟机上运行的结果不一样,那是很大的问题。因此,JMM 与处理器、缓存、并发、编译器有关。它解决了 CPU 多级缓存、处理器优化、指令重排等导致的结果不可预期的问题。

    Java 作为高级语言,屏蔽了 CPU 多层缓存这些底层细节,用 JMM 定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是,JMM 抽象了主内存和工作内存的概念。

一 JMM的主内存和工作内存

在这里插入图片描述
    每个线程只能够直接接触到工作内存,无法直接操作主内存,而工作内存中所保存的正是主内存的共享变量的副本,主内存和工作内存之间的通信是由 JMM 控制的。

  • 所有的变量都存储在主内存中,同时每个线程拥有自己独立的工作内存,而工作内存中的变量的内容是主内存中该变量的拷贝;
  • 线程不能直接读 / 写主内存中的变量,但可以操作自己工作内存中的变量,然后再同步到主内存中,这样,其他线程就可以看到本次修改;
  • 主内存是由多个线程所共享的,但线程间不共享各自的工作内存,如果线程间需要通信,则必须借助主内存中转来完成。

1,JMM主内存的介绍

  • 存储 Java 实例对象,包括成员变量、类信息、常量、静态变量等,但是不包括局部变量和方法参数。
  • 主内存属于数据共享区域,多线程并发操作时会引发线程安全问题。

2,JMM工作内存的介绍

  • 存储当前方法的所有本地变量信息,每个线程只能访问自己的工作内存,每个线程工作内存的本地变量对其他线程不可见
  • 字节码行号指示器、Native 方法等信息
  • 属于线程私有数据区域,不存在线程安全问题

3,主内存和工作内存的数据存储和操作方式

  • 对于实例对象中的成员方法,方法里的基本数据类型的局部变量将直接存储在工作内存的栈帧结构中。方法里引用类型的局部变量的引用在工作内存中的栈帧结构中,对象实例存储在主内存(堆)中。
  • 对于实例对象的成员变量,不管它是基本数据类型或者包装类型(Integer、Double 等)还是引用类型,都会被存储到堆区。
  • 对于实例对象中的静态变量以及类信息都会被存储在主内存中。
  • 需要注意的是,在主内存中的实例对象可以被多个线程共享,如果两个线程调用了同一个对象的同一个方法,两个线程会将数据拷贝到自己的工作内存中,执行完成后刷新回主内存。

在这里插入图片描述

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

二 JMM的内存交互操作

在这里插入图片描述

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

   如果要把一个变量从主内存拷贝到工作内存,那就要按顺序执行 read 和 load 操作,如果要把变量从工作内存同步回主内存,就要按顺序执行 store 和 write 操作。

   注意,Java内存模型只要求上述两个操作必须按顺序执行,但不要求是连续执行。也就是说 read 与 load 之间、store 与 write 之间是可插入其他指令的。如对主内存中的变量 a、b 进行访问时,一种可能出现的顺序是 read a、read b、load b、load a。
内存交互规则:

  • 不允许 read 和 load 、store 和 write 操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现
  • 不允许线程丢弃它最近的 assign 操作,即工作内存中的变量数据改变了之后,必须告知主存。
  • 不允许线程将没有 assign 的数据从工作内存同步到主内存。
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施 use、store 操作之前,必须经过 load 和 assign 操作。
  • 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。
  • 如果对一个变量执行 lock 操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量 前,需要重新执行 load 或 assign 操作以初始化变量的值。
  • 如果一个变量没有被 lock,就不能对其进行 unlock 操作。也不能 unlock 一个被其他线程锁住的变量
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)

三 JMM的三大特征

原子性

   原子性指的是一个操作是不可分割,不可中断的,一个线程在执行时不会被其他线程干扰。

int i = 2;
int j = i;
i++;
i = i + 1;

代码解释:

  • 第一句是基本类型赋值操作,必定是原子性操作。
  • 第二句先读取 i 的值,再赋值到 j,两步操作,不能保证原子性。
  • 第三和第四句其实是等效的,先读取 i 的值,再 +1,最后赋值到 i,三步操作了,不能保证原子性。

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

可见性

   可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。Java 是利用 volatile 关键字来提供可见性的。 当变量被 volatile 修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。而普通变量则不能保证这一点。除了 volatile 关键字之外,final 和 synchronized 也能实现可见性。

  • synchronized 的原理是,在执行完,进入 unlock 之前,必须将共享变量同步到主内存中
  • final 修饰的字段,一旦初始化完成,如果没有对象逸出(指对象为初始化完成就可以被别的线程使用),那么对于其他线程都是可见的。
有序性

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

  • volatile 关键字是使用内存屏障达到禁止指令重排序,以保证有序性。
  • synchronized 的原理是,一个线程 lock 之后,必须 unlock 后,其他线程才可以重新lock,使得被 synchronized 包住的代码块在多线程之间是串行执行的。

四 Volatile和synchronize

1,volatile

很多并发编程都使用了 volatile 关键字(只能用于修饰变量),主要的作用包括两点:

  • 保证线程间变量的可见性:
  • 禁止 CPU 进行指令重排序,通过内存屏障实现。

1)可见性
对 volatile 变量的修改为什么可以做到立即可见?

  • 当写一个 volatile 变量时,JMM 会把对该线程对应的工作内存中的共享变量值刷新到主内存中
  • 当读取一个 volatile 变量时,JMM 会把该线程对应的工作内存置为无效,使得线程只能从主内存中重新读取共享变量

volatile 保证可见性的流程大概就是这样一个过程:
在这里插入图片描述
关于 volatile 变量的可见性,经常会被误解,经常有人会误以为下面的描述是正确的:

  • volatile 变量对所有线程是立即可见的,对 volatile 变量所有的写操作都能立刻反映到其他线程之中。
  • 换句话说,volatile 变量在各个线程中是一致的,所以基于 volatile 变量的运算在并发下是线程安全的。

2)有序性:
Java 内存模型是通过内存屏障(memory barrier)来禁止重排序的volatile 关键字禁止指令重排序有两层意思:

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

3)原子性
这是一个有争议的问题,因为很多人把变量的操作和变量的读取混为一谈了

对变量的读取具有原子性:因为保证了可见性,每次读取一定是内存里面最新值

对变量的操作不具备原子性:这里的操作指的是复合操作,例如(加、减等),因为复合操作代表有着三个过程读取变量的值、对变量进行修改操作、写入内存,整个过程不具备原子性

2,synchronize

   synchronize可以用于修饰方法、代码块。使用JVM的内置锁对当前的代码快进行加锁,保证变量在同一时刻只有一个线程执行,这部分代码块的重排序也不会影响其执行结果。也就是说使用了synchronized可以保证并发的原子性,可见性,有序性。

3,volatile和synchronize的比较
  • 1.volatile是线程同步的轻量级实现,所以volatile的性能要比synchronize好;volatile只能用于修饰变量,synchronize可以用于修饰方法、代码块。随着jdk技术的发展,synchronize在执行效率上会得到较大提升,所以synchronize在项目过程中还是较为常见的;
  • 多线程访问volatile不会发生阻塞;而synchronize会发生阻塞;
  • volatile能保证变量在私有内存和主内存间的同步,但不能保证变量的原子性;synchronize可以保证变量原子性;
  • volatile是变量在多线程之间的可见性;synchronize是多线程之间访问资源的同步性;
  • 对于volatile修饰的变量,可以解决变量读时可见性问题,无法保证原子性。对于多线程访问同一个实例变量还是需要加锁同步。

参考文献:
https://blog.csdn.net/D812359/article/details/124427942
https://blog.csdn.net/weixin_44102992/article/details/127023685

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值