前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试系列之-JMM内存模型(JAVA基础)

面试系列之-JMM内存模型(JAVA基础)

作者头像
用户4283147
发布2023-09-11 15:57:30
1950
发布2023-09-11 15:57:30
举报
文章被收录于专栏:对线JAVA面试对线JAVA面试

JMM概述

JMM定义了一组规则或规范,该规范定义了一个线程对共享变量写入时,如何确保对另一个线程是可见的。实际上,JMM提供了合理的禁用缓存以及禁止重排序的方法,所以其核心的价值在于解决可见性和有序性。

JMM的另一大价值在于能屏蔽各种硬件和操作系统的访问差异,保证Java程序在各种平台下对内存的访问最终都是一致的。Java内存模型规定所有的变量都存储在主存中,JMM的主存类似于物理内存,但有区别,还能包含部分共享缓存。每个Java线程都有自己的工作内存(类似于CPU高速缓存,但也有区别)。

两个概念

主存

主要存储的是Java实例对象,所有线程创建的实例对象都存放在主存中,无论该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括共享的类信息、常量、静态变量。由于是共享数据区域,因此多条线程对同一个变量进行访问可能会发现线程安全问题。

工作内存

主要存储当前方法的所有本地变量信息(工作内存中存储着主存中的变量副本),每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程是不可见的,即使两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括字节码行号指示器、相关Native方法的信息。注意,由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

Java内存模型

  1. 所有变量存储在主存中。
  2. 每个线程都有自己的工作内存,且对变量的操作都是在工作内存中进行的。
  3. 不同线程之间无法直接访问彼此工作内存中的变量,要想访问只能通过主存来传递。

JMM将所有的变量都存放在公共主存中,当线程使用变量时,会把公共主存中的变量复制到自己的工作内存(或者叫作私有内存)中,线程对变量的读写操作是自己的工作内存中的变量副本。因此,JMM模型也需要解决代码重排序和缓存可见性问题。JMM提供了一套自己的方案去禁用缓存以及禁止重排序来解决这些可见性和有序性问题。JMM提供的方案包括大家都很熟悉的volatile、synchronized、final等。JMM定义了一些内存操作的抽象指令集,然后将这些抽象指令包含到Java的volatile、synchronized等关键字的语义中,并要求JVM在实现这些关键字时必须具备其包含的JMM抽象指令的能力。

JMM与JVM物理内存的区别

JMM属于语言级别的内存模型,它确保了在不同的编译器和不同的CPU平台上为Java程序员提供一致的内存可见性保证和指令并发执行的有序性。

类似i++的问题,当多个线程同时访问该共享变量i时,每个线程都会将变量i复制到工作内存中进行修改,如果线程A读取变量i的值时,线程B正在修改i的值,问题就来了:线程B对变量i的修改对线程A而言就是不可见的。这就是多线程并发访问共享变量所造成的结果不一致问题,该问题属于JMM需要解决的问题。

JMM属于概念和规范维度的模型,是一个参考性质的模型。JVM模型定义了一个指令集、一个虚拟计算机架构和一个执行模型。具体的JVM实现需要遵循JVM的模型,它能够运行根据JVM模型指令集编写的代码,就像真机可以运行机器代码一样。

JMM的8个操作

JMM规定所有的变量都存储在主存中(类似于前面讲的主存或者物理内存),每个线程都有自己的工作内存(类似于CPU中的高速缓存)。工作内存保存了线程使用到的变量的拷贝副本,线程对变量的所有操作(读取、赋值等)必须在该线程的工作内存中进行。

8种基本操作时必须满足规则:

(1)不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。不允许read和load、store和write操作之一单独出现,意味着有read就有load,不能读取了变量值而不予加载到工作内存中;有store就有write,也不能存储了变量值而不写到主存中。

(2)不允许一个线程丢弃它最近的assign操作,也就是说当线程使用assign操作对私有内存的变量副本进行变更时,它必须使用write操作将其同步到主存中。

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

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

(5)一个变量在同一个时刻只允许一个线程对其执行lock操作,但lock操作可以被同一个个线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

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

(7)如果一个变量实现没有被lock操作锁定,就不允许对它执行unlock操作,也不允许unlock一个被其他线程锁定的变量。

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

以上JMM的8大操作规范定义相当严谨,也极为烦琐,JVM实现起来也非常复杂。新的JMM版本简化为Read、Write、Lock和Unlock四个操作。

JMM解决有序性问题

1.JMM内存屏障

由于不同CPU硬件实现内存屏障的方式不同,JMM屏蔽了这种底层CPU硬件平台的差异,定义了不对应任何CPU的JMM逻辑层内存屏障,由JVM在不同的硬件平台生成对应的内存屏障机器码。JMM内存屏障主要有Load和Store两类:

(1)Load Barrier(读屏障)

在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主存加载数据。

(2)Store Barrier(写屏障)

在写指令之后插入写屏障,能让写入缓存的最新数据写回主存。在实际使用时,会对以上JMM的Load Barrier和Store Barrier两类屏障进行组合,组合成LoadLoad(LL)、StoreStore(SS)、LoadStore(LS)、StoreLoad(SL)四个屏障,用于禁止特定类型的CPU重排序。

(1)LoadLoad(LL)屏障:在执行预加载(或支持乱序处理)的指令序列中,通常需要显式地声明LoadLoad屏障,因为这些Load指令可能会依赖其他CPU执行的Load指令的结果。

(2)StoreStore(SS)屏障:通常情况下,如果CPU不能保证从高速缓冲向主存(或其他CPU)按顺序刷新数据,那么它需要使用StoreStore屏障。

(3)LoadStore(LS)屏障:该屏障用于在数据写入操作执行前确保完成数据的读取。

(4)StoreLoad(SL)屏障:该屏障用于在数据读取操作执行前,确保完成数据的写入。StoreLoad(SL)屏障的开销是4种屏障中最大的,但是此屏障是一个“全能型”的屏障,兼具其他3个屏障的效果,现代的多核CPU大多支持该屏障。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-09-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 对线JAVA面试 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JMM概述
  • 两个概念
    • 主存
      • 工作内存
      • Java内存模型
      • JMM与JVM物理内存的区别
      • JMM解决有序性问题
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档