当前位置:   article > 正文

Java内存模型详解

java内存模型

一、什么是JMM?为什么需要JMM?

Java 是最早尝试提供内存模型的编程语言。

一般来说,编程语言也可以直接复用操作系统层面的内存模型。不过,不同的操作系统内存模型不同。如果直接复用操作系统层面的内存模型,就可能会导致同样一套代码换了一个操作系统就无法执行了。Java 语言是跨平台的,它需要自己提供一套内存模型以屏蔽系统差异。

Java内存模型(JMM,Java Memory Model) ,是Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。

为什么要遵守这些并发相关的原则和规范呢? 这是因为并发编程下,像 CPU 多级缓存和指令重排这类设计可能会导致程序运行出现一些问题。为此,JMM 抽象了 happens-before 原则(后文会详细介绍到)来解决这个指令重排序问题。

JMM 说白了就是定义了一些规范来解决这些问题,开发发者可以利用这些规范更方便地开发多线程程序。 对于 Java 开发者说,你不需要了解底层原理,直接使用并发相关的一些关键字和类(比如 volatilesynchronized、各种 Lock)即可开发出并发安全的程序。

二、JMM 如何抽象线程和主内存之间的关系?

首先介绍一下什么是主内存?什么是本地内存?

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

Java 内存模型(JMM) 抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中。

Java 内存模型的抽象示意图如下:
在这里插入图片描述
从上图来看,线程 1 与线程 2 之间如果要进行通信的话,必须要经历下面 2 个步骤:

线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。
线程 2 到主存中读取对应的共享变量的值。

也就是说,JMM 为共享变量提供了可见性的保障。

不过,多线程下,对主内存中的一个共享变量进行操作有可能诱发线程安全问题。举个例子:

线程 1 和线程 2 分别对同一个共享变量进行操作,一个执行修改,一个执行读取。
线程 2 读取到的是线程 1 修改之前的值还是修改后的值并不确定,都有可能,因为线程 1 和线程 2 都是先将共享变量从主内存拷贝到对应线程的工作内存中。

关于主内存与工作内存直接的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义了以下八种同步操作:

  • 锁定(lock): 作用于主内存中的变量,将他标记为一个线程独享变量。
  • 解锁(unlock): 作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。
  • read(读取): 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
  • load(载入): 把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中。
  • use(使用): 把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令。
  • assign(赋值): 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储): 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
  • write(写入): 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。

除了这 8 种同步操作之外,还规定了下面这些同步规则来保证这些同步操作的正确执行:

  • 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能在主内存中 “诞生”,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量,换句话说就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。
  • 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。
  • 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。
  • 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量。

三、Java 内存区域和 JMM 有何区别?

这是一个比较常见的问题,很多初学者非常容易搞混。 Java 内存区域和内存模型是完全不一样的两个东西 :

  • JVM 内存结构和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据。 就比如说堆主要用于存放对象实例。
  • Java 内存模型和 Java 的并发编程相关,抽象了线程和主内存之间的关系。 就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。

四、happens-before原则

前面说到JMM指定了一组排序规则,来保证线程之间的可见性。这一组规则即被称为 happens-before

JMM规定,要想保证B操作能够看到A操作的结果(无论他们是否在同一个线程),那么A和B之间必须满足happens-before关系。

JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内存可见性。

为什么需要 happens-before 原则? happens-before 原则的诞生是为了程序员和编译器、处理器之间的平衡。程序员追求的是易于理解和编程的强内存模型,遵守既定规则编码即可。编译器和处理器追求的是较少约束的弱内存模型,让它们尽己所能地去优化性能,让性能最大化。

happens-before 原则的设计思想:

  • 为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果(单线程程序和正确执行的多线程程序),编译器和处理器怎么进行重排序优化都行。
  • 对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。

JSR-133 对 happens-before 原则的定义:

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

可以看下面这个例子:

int userNum = getUserNum(); 	// 1
int teacherNum = getTeacherNum();	 // 2
int totalNum = userNum + teacherNum;	// 3
  • 1
  • 2
  • 3
  • 1 happens-before 2
  • 2 happens-before 3
  • 1 happens-before 3
    虽然 1 happens-before 2,但对 1 和 2 进行重排序不会影响代码的执行结果,所以 JMM 是允许编译器和处理器执行这种重排序的。但 1 和 2 必须是在 3 执行之前,也就是说 1,2 happens-before 3 。

happens-before 原则表达的意义其实并不是一个操作发生在另外一个操作的前面,虽然这从程序员的角度上来说也并无大碍。更准确地来说,它更想表达的意义是前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。

举个例子:操作 1 happens-before 操作 2,即使操作 1 和操作 2 不在同一个线程内,JMM 也会保证操作 1 的结果对操作 2 是可见的。

happens-before 常见规则有哪些?
happens-before 的规则有 8 条,重点讲解下面列举的 6 条:

  • 单线程规则。 一个线程中的每个动作都happens-before该线程中后续的每个动作。
  • 监视器锁定规则。 监视器的解锁动作happens-before后续对这个监视器的锁定动作。
  • volatile变量规则。 对volatile字段的写入动作happens-before后续对这个字段的读取动作。
  • 线程start规则。 线程start()方法的执行happens-before一个启动线程内的任意动作。
  • 线程join规则。 一个线程内的所有动作happens-before任意其他线程在该线程join()成功返回之前。
  • 传递规则。 如果A happens-before B,且B happens-beforeC,那么A happens-before C。

如果两个操作不满足上述任意一个 happens-before 规则,那么这两个操作就没有顺序的保障,JVM 可以对这两个操作进行重排序。

五、总结

  • Java 是最早尝试提供内存模型的语言,其主要目的是为了简化多线程编程,增强程序可移植性的。
  • CPU 可以通过制定缓存一致协议(比如 MESI 协议) 来解决内存缓存不一致性问题。
  • 为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
  • JMM 除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范。
  • JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内存可见性。

参考链接:
https://javaguide.cn/java/concurrent/jmm.html#%E4%BB%8E-cpu-%E7%BC%93%E5%AD%98%E6%A8%A1%E5%9E%8B%E8%AF%B4%E8%B5%B7

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/运维做开发/article/detail/875691
推荐阅读
相关标签
  

闽ICP备14008679号