JVM learning from scratch -- Java Memory Model (JMM)

basic introduction

Why have a memory model?

To answer this question, we must first understand the computer hardware memory structure

computer hardware memory architecture

4-core CPU read memory data model

image-20221002185757916

explain

  • CPU Register

    What are CPU registers?

    The CPU register is a very small and very fast storage part inside the CPU, and its capacity is very limited. Modern CPUs have dozens or even hundreds of registers built into them. Generally, when we say how many bits of PC are, we actually refer to the number of bits of CPU registers.

    Why CPU registers?

    Although the reading speed of memory is very fast, compared with CPU, there is still a big gap, not an order of magnitude. If the data is read from memory every time, it will seriously slow down the running speed of the CPU, and the CPU is often in a waiting state with nothing to do. Setting up a cache inside the CPU can temporarily read frequently used data into the cache. When data at the same address is needed, there is no need to go all the way to access the memory, and it can be read directly from the cache.

  • Cache Memory

    What is Cache memory?

    Cache memory is located between the CPU and main memory, and is small in scale but high in speed. Usually consists of SRAM(static memory).

    Why Cache Memory?

    The speed of the CPU is much higher than that of the memory. When the CPU directly accesses data from the memory, it needs to wait for a certain period of time, while the Cache can save a part of the data that the CPU has just used or recycled. If the CPU needs to use this part of the data again, it can be Called directly from the Cache, thus avoiding repeated access to data, reducing the waiting time of the CPU, thus improving the efficiency of the system.

Due to the order of magnitude gap between the main memory and the computing power of the CPU, a cache is introduced in the traditional computer memory architecture as a buffer between the main memory and the processor. After the end, the CPU will synchronize the operation result to the main memory.

Using the cache solves the problem of CPU and main memory speed mismatch, but at the same time introduces another new problem: 缓存一致性问题.

cache coherency issues

What are cache coherency issues?

在多CPU的系统中(或者单CPU多核的系统),每个CPU内核都有自己的高速缓存,它们共享同一主内存(Main Memory)。当多个CPU的运算任务都涉及同一块主内存区域时,CPU 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致。

因此需要每个 CPU 访问缓存时遵循一定的协议,在读写数据时根据协议进行操作,共同来维护缓存的一致性。这类协议有 MSI、MESI、MOSI、和 Dragon Protocol 等。

为了提升性能在 CPU 和主内存之间增加了高速缓存,但在多线程并发场景可能会遇到缓存一致性问题。那还有没有办法进一步提升 CPU 的执行效率呢? 答案是:处理器优化

处理器优化和指令重排序

什么是处理器优化?

为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化。

除了处理器会对代码进行优化处理,很多现代编程语言的编译器也会做类似的优化,比如像 Java 的即时编译器(JIT)会做指令重排序

image-20220928141640358

重排序的类型

  • 编译器优化的重排序:

    编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。

  • 指令级并行的重排序:

    现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  • 内存系统的重排序:

    由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

在解决完以上的问题以后,我们开始真正的要解决「为什么要有内存模型?」的问题了。

我们上面讲的「缓存一致性」、「处理器优化」、「指令重排序」,分别就是造成我们Java并发编程的三个基本问题:「可见性问题」、「原子性问题」和「有序性问题」的主要原因。

为了解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。我们在物理机器上定义出一套内存模型,规范内存的读写操作。内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。

Java 内存模型(JMM)

首先最重要的一点——「Java 内存模型(JMM)」指的不是 「Java运行时数据区域」!

「Java 运行时数据区域」是我们看八股文常看到的:" Java内存区域五大块:堆、方法区、虚拟机栈、本地方法栈、PC寄存器... "

「Java 内存模型(JMM)」是什么在本章会做解答。接着往下看吧!

什么是 JMM

JMM(Java Memory Model)是Java内存模型,用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。

JMM 本身是一种抽象的概念,实际上不存在,它描述的是一组规范。通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM 规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

JMM 的几个规范

  • 所有变量存储在主内存
  • 主内存是虚拟机内存的一部分
  • 每条线程有自己的工作内存
  • 线程的工作内存保存变量的主内存副本
  • 线程对变量的操作必须在工作内存中进行
  • 不同线程之间无法直接访问对方工作内存中的变量
  • 线程间变量值的传递均需要通过主内存来完成

Java运行时内存区域与硬件内存的关系

Java 运行时内存区域是分成栈和堆的,但这些都是JVM定义的逻辑概念。在传统的硬件内存架构中没有栈和堆这种概念。

Java 运行时内存区域与硬件之间的关系图

image-20221002191805847

由上图可知,栈和堆既存在于高速缓存中又存在于主存中。

这两者在硬件上的分布并不是隔离的,都不过是JVM在存储设备上的抽象划分。

Java线程与主存的关系

JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域。

JMM 规定了所有变量都必须存储在主存中。主内存是共享内存区域,所有线程都可以访问。但是线程对变量的操作都必须在工作内存中进行,首先需要将变量从主存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存。

线程不能直接操作主存中的变量,各个线程中的工作内存中存储着主存中的变量拷贝副本,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主存来完成。

JMM 规范定义的几个规范,并且根据几个规范作图

  • 所有的变量都存储在主内存中。
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。
  • 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
  • 不同的线程之间无法直接访问对方本地内存中的变量。

image-20221002190823796

线程之间拥有的本地内存是逻辑上隔离,而物理上不隔离的。

JMM下的线程间通信

接下来我们作图演示如下操作:

两个线程都对一个共享变量进行操作,共享变量初始值为 1,线程1对它进行加 1,接着线程2对它进行加1,预期共享变量的值为 3。

JMM 规范下的操作:

image-20221002192522693

  • 步骤一:

    线程1从主存中读取A,进行加一更新,将本地内存1中更新过的共享变量A(此时为2)刷新到主存中去

  • 步骤二:

    线程2从主存中读取线程1更新过的共享变量A(此时为2),并进行加一更新,此时为3,再刷新到主存中去。

JMM数据原子操作

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

  • lock

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

    加锁时的操作。

  • unlock

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

  • read

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

  • load

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

  • use

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

  • assign

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

  • store

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

  • write

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

注意:工作内存也就是本地内存的意思。

八种操作的要点

  • JVM 实现时必须保证上面的每种操作都是原子的
  • JMM 只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
  • 如果要把一个变量从主存中复制到工作内存中,则需要按序执行readload操作。
  • 如果要把一个变量从主存中同步回主存中,则需要按序执行storewrite操作。

交互操作的流程

按照:「主内存→read→load→工作内存→use→业务方法→assign→工作内存→store→write→主内存」的顺序进行交互

image-20221002213731899

解释

  • 什么是总线?

    这里的总线实际上就是硬件之间的关联。硬件之间的数据传递靠的就是总线,靠着总线的概念我们在研究问题的时候可以忽略背后复杂的硬件结构。

  • readloaduseassign

    光画图和文字可能还是比较抽象,这里我直接写好了代码,可以和图结合理解。

    这里我们定义了一个线程1:

    image.png

    线程1运行并遇到initFlag,则依次执行readload操作,将变量从主存中加载到工作内存中。

    然后我们看到!initFlag这个语句就是进行use操作了,对工作内存读取数据到线程里面来进行计算。

    然后根据代码的循环逻辑CPU就卡在这里不断地对initFlag做取反操作

    同时我们这里还有一个线程2,一样地要对initFlag进行readloaduse。关键在于对initFlag做了一次赋值操作assign

    image.png

    这个时候工作内存中的initFlag已经从false变成true了。

  • storewrite

    线程2赋值完成以后,共享变量initFlag的副本值会同步回我们的主存

    image.png

    这个同步的过程底层做的操作就是store,把工作内存中的变量的值传送到主存中。

    但是这个时候只是暂时放入到主存,并没有赋值给主存中的共享变量。直到它进行了write操作。

    在这之后主存中的initFlag才会变成true

关于缓存一致性问题的详细解决方法会在后面多线程章节的博客里解决。

小结

This chapter is an introductory chapter for JVMlearning, and JUChas a strong correlation with the concurrent programming chapter. It can be reviewed and summarized repeatedly according to the subsequent learning.

Reference for this article:

Supongo que te gusta

Origin juejin.im/post/7150174931970424869
Recomendado
Clasificación