JMM说明

发布时间 2023-05-04 20:58:13作者: 秋风下的落叶

JMM(Java内存模型)是一种定义了多线程之间共享数据、以及数据读写时的可见性和有序性的规范。JMM规范是建立在操作系统内存模型之上的,是Java语言对于并发编程的一种抽象,规范了Java程序在并发情况下内存访问的行为。

Java内存模型主要包含以下几个概念:

  • 主内存:Java虚拟机中的主内存是所有线程共享的内存区域,所有变量的值都存储在主内存中。

  • 工作内存:Java虚拟机中的每个线程都有自己的工作内存,工作内存中存储了主内存中的变量副本。

  • 内存间交互操作:Java虚拟机定义了一组原子性的操作,用于实现线程之间的内存交互,例如lock、unlock、read、write等。

  • Happens-before关系:用于描述程序中操作之间的顺序关系,如果一个操作在happens-before于另一个操作,那么在程序中这两个操作是有顺序的。

Java内存模型主要是为了保证多线程之间的内存可见性和有序性。在Java中,多个线程可以同时访问共享的变量,如果不采取一些措施,就会出现内存读写不一致的问题。Java内存模型通过保证线程之间的内存可见性和有序性,从而避免了这些问题的发生。

总的来说,Java内存模型规范了Java程序在并发情况下内存访问的行为,包括变量的可见性、指令的有序性等,对于理解Java并发编程非常重要。

什么是主内存?什么是本地内存?

主内存: 所有线程创建的实例对象都存放在主内存中,不管该对象是 成员变量 还是 局部变量.
本地内存: 每个线程都有自己的私有本地内存用来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存.本地内存是JMM抽象出来的概念,存储了主内存中的共享变量副本.

注意这里讲的是本地内存,但是有的资料讲的是工作内存,感觉工作内存这个概念更加符合,本地内存让我想到JVM运行时数据区的模型了.
找了一圈,没找到在哪里讲到, 暂时可以把本地内存和工作内存看作是一样的吧
JSR133规范
可以看到JSR133规范并没有讲到什么主内存,本地内存或者工作内存,那么几个概念是在哪里给出来的呢? 不可能由博主自己想出来的,肯定有一个出处.
JDK8文档
暂时在这里也没有找到关于工作内存的说明
Java虚拟器规范
三个文档都没有找到明显关于本地内存或者工作内存的说明,这就比较奇怪了.

主内存与工作内存的交互过程

一个变量如何从主内存拷贝到工作内存, 如何从工作内存同步到主内存当中? JMM定义了以下8种同步步骤

  1. 锁定lock: 作用于主内存中的变量,标志该变量为一个线程独享变量
  2. 读取read: 作用于主内存的变量,就是把变量从主内存读取到工作内存当中.
  3. 载入load: 把read操作得到的变量放入到工作内存的变量副本中.也就是完成工作内存的共享变量副本的copy操作.
  4. 使用use: 把工作内存中的一个变量的值传递给执行引擎,就是虚拟机使用该变量时,都会调用该指令.
  5. 赋值assign: 作用于工作内存中的变量,把一个从执行引擎接收到的值赋给工作内存中的变量.
  6. 存储store: 作用于工作内存的变量,就是把工作内存的变量传送到主内存当中.
  7. 写入write: 作用于主内存的变量, 就是把从工作内存得到的变量写入到主内存当中.
  8. 解锁unlock: 作用于主内存中的变量,只能解锁的变量,才能被其他线程占用.

上面是JMM内存模型的一个工作过程,可以结合图片来看, 不需要硬背这个过程,因为它的基本流程还是比较清楚的.
除了上面的8个同步步骤,还规定了一些规则用来保证这些同步操作正确执行.

  1. 不允许一个线程不经过assign操作就直接把变量同步到主内存当中,也就是直接执行store操作.

想想也是,如果线程只是读取共享变量,没有进行赋值操作,完全没必要更新主内存的变量值.

  1. 一个新的共享变量只能在主内存诞生,不允许直接在工作内存当中使用未被初始化的变量.

我们可以这么理解,当一个变量在主内存初始化了,如果一个线程使用到该变量,就需要从主内存read和load变量到工作内存, 从而保证线程间使用的变量是相同的, 如果直接在工作内存对变量进行初始化, 因为主内存并没有对应的值,这样多线程可能会同时进行初始化,导致变量的不一致性.

  1. 一个变量在同一时刻只允许一个线程对其lock,但是lock操作可以被同一个线程多次执行, 多次执行lock操作,之后也需要多次执行相同的unlock操作

为什么lock操作可以被同一个线程多次执行? 什么场景下需要多次lock操作呢?
一个线程可能会多次lock操作是因为在其执行期间可能存在多个临界区,每个临界区都需要对该变量进行保护。例如,在生产者-消费者模型中,生产者和消费者都需要对共享Buffer进行保护,因此每个线程在进入临界区之前需要先执行lock操作以确保只有一个线程可以访问共享资源。如果多个临界区相互嵌套,那么同一个线程可能会多次进行lock操作以保证每次访问共享资源都是安全的。此外,如果某个线程拥有该变量的lock,在多次进入临界区时,它可以重复执行lock操作,以保证它依然拥有该变量的lock。当这个线程结束其临界区操作时,需要与之匹配的是相同数量的unlock操作,以确保该变量的lock可以被其他线程获得。
注意这里讲的是多次lock操作,并不是指多次对同一个变量进行lock操作

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

这里清空工作内存中的变量值,是指所有线程的值,还是执行lock操作的线程的工作内存呢?
这种清空操作为什么在8个同步步骤当中没有显示出来.
对这条规则有所怀疑

  1. 如果一个变量事先没有执行lock操作,则不能直接对其进行unlock操作,也不能对其他线程lock的变量进行unlock.
  2. 对一个变量时行unlock前,必须先将变量刷新到主内存当中.

Java内存区域与JMM有何区别?

Java内存区域, 一般指Java运行时的内存区域,主要定义了JVM运行时如何进行数据存储
JMM与java并发编程有关,是一套保证并发编程的可见性和有序性的规范.它抽象了线程和主内存之间的关系,规定了从java源代码到CPU可执行指令的这个转化过程需要遵守哪些和并发相关的原则和规范,主要目的是为了简化并发编程,增强程序可移植性.

happen-before原则

happen-before原则的设计思想

  1. 为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果,编译器和处理器怎样进行指令重排都可以
  2. 对于会改变程序执行结果的指令重排,JMM要求编译器和处理器必须禁止这种重排.

heppen-before原则定义

  1. 如果一个操作happen-before另一个操作,那么第一个操作的执行结果对第二个操作可见,并且第一个操作的执行顺序在第二个操作之前.
  2. 两个操作间存在happen-before关系,并不意味着java平台的具体实现必须按照happen-before关系指定的顺序执行,如果重排序之后的执行结果,与按happen-before关系来执行的结果一致,那么JMM也允许这样的重排序

怎么感觉第一原则与第二原则有矛盾, 两个操作间存在happen-before关系,无非就是A操作happen-beforeB操作,或者B操作happen-beforeA操作.
根据第一原则,就肯定A操作在B操作之前执行,或者B操作在A操作之前执行,这是happen-before关系指定的顺序,但是JMM规定如果指令重排后,执行结果一致,那么这种重排也允许的,就是是JMM把happen-before放得更加宽了.

例子

int a = 1; //A操作
int b = 2; //B操作
int c = a + b;//C操作

请看上面的例子:A操作happen-before B操作,B操作happen-before C操作, A操作happen-before C操作,对于A操作和B操作,其实存在happen-before关系,但是重排序后对执行结果没有影响,所以JMM允许这种重排序.

happen-before原则

  1. 程序顺序规则:其实就是在同一个线程当中,程序按顺序执行
  2. volatile变量规则:对一个volatile变量的写操作 Happens-Before 于任何后续对该变量的读写操作。其实就是volatile变量写操作优先
  3. 传递性:如果操作A Happens-Before 操作B,操作B Happens-Before 操作C,则操作A Happens-Before 操作C。
  4. synchronized锁规则:一个unlock操作 Happens-Before 于之后对同一个锁的lock操作。

其实就是如果想对一个已经lock的变量再次lock的话,需要先unlock操作
happen-before可以理解成发生在什么操作之前,所以上面应该解读成一个unlock操作发生在之后对同一个锁的lock操作之前

  1. 线程启动规则:Thread对象的start方法 Happens-Before 于启动的线程执行任何操作。
  2. 线程终止规则:线程的所有操作 Happens-Before 于其他线程检测到该线程已经终止。

其他线程检测到该线程已经终止之前,线程的所有操作应该已完成

  1. 线程中断规则:对线程调用interrupt方法 Happens-Before 于被中断线程的执行被响应。
  2. 对象终结规则:一个对象构造函数的结束 Happens-Before 于该对象的 finalize 方法的开始。

这些规则帮助Java程序员了解在多线程编程中操作之间的时间顺序关系,并且确保操作发生的正确顺序。遵循这些规则可以避免一些常见的多线程问题,如死锁、竞态条件和数据损坏等。

happen-before与JMM的关系

一个happen-before规则对应于一个或多个编译器和处理器重排序规则.

并发的三个重要特性

原子性: 一次操作或者多次操作的时候,要么所有操作都一起成功,要么所有操作一起失败, 通过sychronized,lock以及各种原子类实现来保证
有序性: 由于指令重排序问题,代码的执行顺序未必就是代码的书写顺序, 通过volatile关键字保证
可见性: 当一个线程对某个共享变量进行修改, 其他线程立马可以收到最新的值, 通过sychronized, lock, volatile来保证