你真的了解Java内存模型JMM吗?

java,jmm · 浏览次数 : 0

小编点评

Java 内存模型(JMM)是 Java 虚拟机规范中一组组用于定义 Java 应用程序中变量存储规则和线程同步机制的规则。其主要目标是使得 Java 程序在多线程环境下能够达到和单线程环境同样的性能,同时保证数据的可见性、一致性和原子性。 Java 内存模型主要涉及以下几个方面: 1. 硬件内存结构:包括 CPU 寄存器、CPU 缓存(如 L1、L2 缓存)和主内存。 2. 缓存一致性问题和指令重排序:为了解决内存缓存不一致性问题,Java 内存模型引入了缓存一致性协议(如 MESI 协议),以确保多线程环境下变量访问的一致性。此外,编译器优化重排和处理器指令并行重排也会影响代码的执行顺序。 3. Java 内存与主内存:Java 中的所有变量都存储在主内存中,而每个线程有自己的工作内存。线程对变量的操作必须在工作内存中进行,不能直接读写主内存中的变量。线程间变量值的传递需要通过主内存来完成。 4. 八种操作:Java 内存模型定义了八种操作来实现线程间的通信,包括 lock、unlock、read、write、load、use、assign 和 store 操作。 在 Java 中,为了保证数据的可见性、一致性和原子性,可以使用以下方法: 1. 使用 volatile 关键字:可以禁止指令重排序优化,从而确保共享变量的可见性。 2. 使用 synchronized 关键字或 Lock 接口:可以实现线程间的同步,保证原子性和可见性。 3. 使用原子类(如 java.util.concurrent.atomic 包中的类):提供原子性的操作,如 increment、getAndSet 等。 总之,Java 内存模型是一个复杂的概念,需要深入理解其原理和实现,以便在实际开发中避免出现内存访问和同步问题。

正文

面试官

哈喽,大家好🎉,我是世杰

本文我为大家介绍面试官经常考察的「Java内存模型JMM相关内容」

面试连环call

  1. 什么是Java内存模型(JMM)? 为什么需要JMM?
  2. Java线程的工作内存和主内存各自的作用?
  3. Java缓存一致性问题?
  4. Java的并发编程问题?

要想理解透彻 JMM(Java 内存模型),我们先要从 『硬件内存结构』 说起。让我们开始吧!🎉🎉🎉

1. 硬件内存结构

1.1 CPU 缓存模型

img

(1)CPU Register

CPU Register 也就是 CPU 寄存器。CPU 寄存器是 CPU 内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级。

(2)CPU Cache Memory

CPU Cache Memory 也就是 CPU 高速缓存,相对于寄存器来说,通常也可以成为 L2 二级缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 CPU 还是相差数量级,所以在 CPU 和主存间引入了多级缓存。CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。

(3)Main Memory

Main Memory 就是主存,主存比 L1、L2 缓存要大很多。

1.2 缓存一致性问题

由于主存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲。先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。

但是,这样存在 内存缓存不一致性的问题 !比如我执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 i++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。

CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议(比如 [MESI 协议open in new window])或者其他手段来解决。 这个缓存一致性协议指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范。不同的 CPU 中,使用的缓存一致性协议通常也会有所不同。

img

1.3 指令重排序

什么是指令重排序? 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。

常见的指令重排序有下面 2 种情况:

  • 编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
  • 指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序:由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

img

Java 源代码会经历 编译器优化重排 —> 指令并行重排 —> 内存系统重排 的过程,最终才变成操作系统可执行的指令序列。


2. Java内存模型

2.1 Java内存与硬件内存

之前讲 JVM 运行时内存区域时,聊到 JVM 分为栈、堆等,这些都是 JVM 定义的概念。在传统的硬件内存架构中是没有栈和堆这种概念。从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以Java内存和硬件内存没有直接的关系。

img

2.2 Java内存与主内存

(1) Java 内存模型规定所有的变量都存储在主内存中,每条线程都有自己的工作内存。

(2) 线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。

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

jmm

  • 主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量,还是局部变量类信息常量静态变量都是放在主内存中。为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。

  • 本地内存:每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。每个线程只能操作自己本地内存中的变量,无法直接访问其他线程的本地内存。如果线程间需要通信,必须通过主内存来进行。本地内存是 JMM 抽象出来的一个概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:

『主内存』

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

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

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

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

『工作内存』

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

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

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

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


3. 再聊并发编程

熟悉 Java 并发编程的同学肯定对这三个问题很熟悉:『可见性问题』『原子性问题』『有序性问题』

3.1 有序性

由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。我们上面讲重排序的时候也提到过:指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。

在 Java 中,volatile 关键字可以禁止指令进行重排序优化。

3.2 原子性

一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行

在 Java 中,可以借助synchronized、各种 Lock 以及各种原子类实现原子性。

3.3 可见性

当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。

在 Java 中,可以借助synchronizedvolatile 以及各种 Lock 实现可见性。


参考文章

  1. Java 内存模型引入

  2. JMM(Java 内存模型)详解

  3. 说说什么是Java内存模型?

  4. 从 CPU 讲起,深入理解 Java 内存模型!

与你真的了解Java内存模型JMM吗?相似的内容:

你真的了解Java内存模型JMM吗?

面试连环call: 1. 什么是Java内存模型(JMM)? 为什么需要JMM? 2. Java线程的工作内存和主内存各自的作用? 3. Java缓存一致性问题? 4. Java的并发编程问题?

java多线程编程:你真的了解线程中断吗?

java.lang.Thread类有一个 interrupt 方法,该方法直接对线程调用。当被interrupt的线程正在sleep或wait时,会抛出 InterruptedException 异常。事实上, interrupt 方法只是改变目标线程的中断状态(interrupt status),...

我在前端写Java SpringBoot项目

本篇文章主要是使用 NestJs + Sequelize + MySQL 完成基础运行, 带大家了解 Node 服务端的基础搭建,也可以顺便看看 Java SpringBoot 项目的基础结构,它俩真的非常相似,不信你去问服务端开发同学。

3种分页列表缓存方式,速收藏~

摘要:本文介绍了实现分页列表缓存的三种方式。 本文分享自华为云社区《分页列表缓存,你真的会吗》,作者: 勇哥java实战分享 。 1 直接缓存分页列表结果 显而易见,这是最简单易懂的方式。 我们按照不同的分页条件来缓存分页结果 ,伪代码如下: public List getPage

CompletableFuture学习总结

CompletableFuture 简介 在Java8中,CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合 CompletableFuture 的方法。 Java

《HelloTester》第4期

1.前言 终于到了谈面试的部分了! 我在这也说明一下,有同学说之前简历篇的时候一直在说项目的介绍,而面试官真正关心的是技术啊?我在这做个解释,因为我写的这些文章主要针对的是软件测试的同学,所以其他职位的请根据自己的情况来改,比如你是面的前端或者java等,那当然要突出你在编程中的表现了! 首先来说,

[转帖]你真的了解nf_conntrack么?

https://blog.51cto.com/u_15293891/3290242 女主宣言 该文章出自HULK虚拟化团队(网络小分队),主要是基于在奥创版本升级过程中遇到的一个nf_conntrack问题展开的。该问题在日常开启了iptables的高并发运维场景中也会经常出现。该文章主要是结合实际

[转帖]按压硬盘能提升几十MB/s 的硬盘读写速度,长见识了

https://www.ittel.cn/archives/6326.html 现在基本每台电脑都配置了 SSD (固态硬盘),但如果用在服务器、NAS 等大容量存储设备中,HDD(机械硬盘)的性价比依然无人能敌。 一方面是因为它便宜量大,一方面是因为它可靠性强,数据存储时间长。而你是否真的了解 H

CSS 属性计算

CSS 属性计算过程 你是否了解 CSS 的属性计算过程呢? 有的同学可能会讲,CSS属性我倒是知道,例如: p{ color : red; } 上面的 CSS 代码中,p 是元素选择器,color 就是其中的一个 CSS 属性。 但是要说 CSS 属性的计算过程,还真的不是很清楚。 没关系,通过此

如何正确使用 ThreadLocal,你真的用对了吗?

本文主要从源码的角度解析了 ThreadLocal,并分析了发生内存泄漏的原因及正确用法,最后对它的应用场景进行了简单介绍。