原创

详细讲解Java内存模型

温馨提示:
本文最后更新于 2021年02月04日,已超过 553 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

Java内存模型(Java Memory Model,JMM)是Java虚拟机规范定义的,用来屏蔽掉Java程序在各种不同的硬件和操作系统对内存的访问的差异。

常常和Java内存区域混淆,这是两个不同的概念。

Java内存区域是指 JVM运行时将数据分区域存储 ,简单的说就是不同的数据放在不同的地方。通常又叫 运行时数据区域

参考上一篇:8张图 带你理解Java内存区域

1、为什么要有Java内存模型?

简单的说是屏蔽硬件的差异

Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异。——《深入理解Java虚拟机》

Java程序运行在不同配置的服务器上,CPU和内存的配置都不一样,如何保证数据的一致性,就需要Java内存模型了,定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。

CPU和内存是不直接通讯的,因为两者的运行效率是不一样的,为了提高效率,计算机引入高速缓存来充当介质。在多核CPU中,每个CPU都拥有自己的缓存,那同一个数据,在CPU各自的高速缓存中,以及内存中,可能就不一致了。

为了解决这一问题,又引出了缓存一致性协议(MESI)。在读写时要根据协议进行操作,来维护缓存的一致性。

MESI的详解可参考:https://www.cnblogs.com/yanlong300/p/8986041.html

CPU和内存这么复杂,在写Java程序的时候, 跨平台部署,都要考虑底层的硬件差异,程序员肯定不干啊,那这时候,就出现了Java内存模型,充当一个媒介。

2、Java内存模型介绍

Java虚拟机带来的Java内存模型,定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。

站在Java程序员的角度:Java内存模型规定了不同线程如何以及何时可以看到其他线程写入共享变量的值以及如何在必要时同步对共享变量的访问

每个处理器内核拥有私有的高速缓存,JMM 中每个线程拥有私有的本地内存

这里说的 主内存、工作内存 和Java中堆、栈、方法区不是同一个概念,这两者没有什么关系。在系统内存架构中并没有栈(stack)、堆(heap)这种概念,只有寄存器(register)、缓存(cache)、主内存(RAM、Main Memory)。

如果要强行联系,你可以把Java中的堆当成主内存,栈当成工作内存。

堆中的变量如果在多线程中都使用,就涉及到了堆和不同虚拟机栈中变量的值的一致性问题了。

  • 主内存:java虚拟机规定所有的变量都必须在主内存(RAM)中产生,为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存(RAM)相比,只不过物理机的主内存(RAM)是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。
  • 工作内存:java虚拟机中每个线程都有自己的工作内存(JMM抽象出来的),该内存是线程私有的为了方便理解,可以认为是虚拟机栈。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。

理论上说Java所有的 栈 和 堆 都存储在物理主内存中,但随着CPU运算其数据的副本可能被缓存或者寄存器 持有

3、内存交互

大致流程:

线程在JVM下交互过程

线程1和线程2都有主内存中共享变量x的副本,初始时,这3个内存中x的值都为0。线程1中更新x的值为1之后同步到线程2主要涉及2个步骤:

  • 1、 线程1把线程工作内存中更新过的x的值刷新到主内存中。
  • 2、 线程2到主内存中读取线程1之前已更新过的x变量。

大致流程看着是挺简单的,工作内存同步回主内存之类的实现细节,其实也不复杂,主要是通过以下8种操作指令完成的,而且这8个操作必须是原子性的。

  • lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线程独占这个变量
  • unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定
  • read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
  • load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)
  • use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作
  • assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作
  • store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
  • write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。

Java内存模型 - 同步操作与规则

如上图所示,把一个变量数据从主内存复制到工作内存,要顺序执行 readload

而把变量数据从工作内存同步回主内存,就要顺序执行 storewrite 操作。

4、并发内存模型

上述提到的内存交互过程,在多线程并发情况下,数据就会有非一致性问题。

以上述例子来说,线程1、线程2同时向主存取值x,x初始值为0,同时加1,线程1更新x的值为1之后,写到主存;线程2还没拿到x的最新值,又加1,把1又回写主存,就样就会有脏数据。

并发模型为了解决这一问题,设计了三个特性:

  • 原子性

    由Java内存模型来直接保证原子性的变量操作包括read、load、use、assign、store、write这6个动作,虽然存在long和double的特例,但基本可以忽略不计,目前虚拟机基本都对其实现了原子性。

如果需要更大范围的控制,lock和unlock也可以满足需求。lock和unlock虽然没有被虚拟机直接开给用户使用,但是提供了字节码层次的指令monitorentermonitorexit对应这两个操作,对应到java代码就是synchronized关键字,因此在synchronized块之间的代码都具有原子性。

注意:volatile并不能解决原子性问题。

  • 可见性

    可见性是指一个线程修改了一个变量的值后,其他线程立即可以感知到这个值的修改。正如前面所说,volatile类型的变量在修改后会立即同步给主内存,在使用的时候会从主内存重新读取,是依赖主内存为中介来保证多线程下变量对其他线程的可见性的。

Java最常用的就是提供volatile保持可见性,synchronizedfinal也可以实现可见性。synchronized关键字是通过unlock之前必须把变量同步回主内存来实现的,final则是在初始化后就不会更改,所以只要在初始化过程中没有把this指针传递出去也能保证对其他线程的可见性。

  • 有序性

    有序性从不同的角度来看是不同的。单纯单线程来看都是有序的,但到了多线程就会跟我们预想的不一样。

可以这么说:如果在本线程内部观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句说的就是“线程内表现为串行的语义”,后半句指的是“指令重排序”现象和主内存与工作内存之间同步存在延迟的现象。

保证有序性的关键字有volatilesynchronizedvolatile禁止了指令重排序,而synchronized则由“一个变量在同一时刻只能被一个线程对其进行lock操作,串行操作”来保证。


5、总结

Java内存模型定义了线程和内存间的抽象关系,在硬件的体现就是cpu核线程,高速缓存和主存间的关系。在并发情况下,Java通过引入synchronized和volatile解决 可见性、有序性 问题。

Java内存模型

参考:

正文到此结束
关注公众号 【HelloCoder】
免费领取Java学习资料
让技术,化繁为简
本文目录