小蔡学Java

JMM (Java内存模型)

2024-09-23 21:00 904 0 JVM / JUC JVM

认识JMM

概念介绍

JMM是Java内存模型 ( Java Memory Model),简称JMM。它本身只是一个抽象的概念,并不真实存在,它描述的是一种规则或规范,是和多线程相关的一组规范。通过这组规范,定义了程序中对各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。需要每个JVM 的实现都要遵守这样的规范,有了JMM规范的保障,并发程序运行在不同的虚拟机上时,得到的程序结果才是安全可靠可信赖的。

Java内存模型围绕着在并发过程中如何处理可见性、原子性、有序性这三个特性而建立的模型。

JMM产生的背景

JMM(Java内存模型)源于物理机器CPU架构的内存模型,最初用于解决MP(多处理器架构)系统中的缓存一致性问题,而JVM为了屏蔽各个硬件平台和操作系统对内存访问机制的差异化,提出了JMM的概念。Java内存模型是一种虚拟机规范,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在并发时如何同步的访问共享变量。通过这种方式来保证多线程下变量的缓存一致性问题。

JMM结构规范

JMM规范(JSR-133)

即JavaTM内存模型与线程规范,由JSR-133专家组开发。本规范是JSR-176(定义了JavaTM平台 Tiger(5.0)发布版的主要特性)的一部分。本规范的标准内容将合并到JavaTM语言规范、JavaTM虚拟机规范以及java.lang包的类说明中。

具体内容参考《JSR133中文版1.pdf》

JMM结构

JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

如图所示:

说明:线程间通信,主要是通过共享变量的形式实现。

主内存和工作内存工作流程

如图所示:

JMM的八种原子操作:

为了支持 JMM,Java 定义了8种原子操作,用来控制主存与工作内存之间的交互:

  • read 读取:作用于主内存,将共享变量从主内存传送到线程的工作内存中。

  • load 载入:作用于工作内存,把 read 读取的值放到工作内存中的副本变量中。

  • store 存储:作用于工作内存,把工作内存中的变量传送到主内存中。

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

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

  • assign 赋值:作用于工作内存,把执行引擎获取到的值赋值给工作内存中的变量,当虚拟机栈遇到给变量赋值的指令时,就执行此操作。

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

  • unlock解锁: 作用于主内存,它将释放独占状态。

举例说明,如下图

解析:

一、线程加载变量到工作内存流程:

1、虚拟机从主内存中读取变量int age=20,首先使用read读取,将共享变量从主内存传送到线程的工作内存中,此时,主内存加锁,是lock状态。

2、调用load载入操作,把 read 读取的值int age=20放到工作内存中的副本变量中。

3、虚拟机调用use操作,把工作内存的值传递给执行引擎(线程2),执行引擎拿到了变量int age=20。

二、线程修改变量写回主存流程:

1、执行引擎(线程2)通过assign 赋值操作,把工作内存中的变量int age 修改为22。

2、虚拟机调用store操作,将修改的变量int age=22传送到主内存中。

3、虚拟机调用write操作,把从工作内存中 store 传送过来的值int age=22写到主内存的变量中,操作完成后,释放主内存中的锁,状态变更为unloack。

说明:在多线程并发操作主内存的同一个变量时,如上图所示,线程1和线程2同时加载了变量int age=20,这时候,他们之间会通过CPU总线嗅探机制,确定一方的缓存会失效,失效的一方修改的变量值从工作内存中刷回主存会失败。这是通过计算机的MESI缓存一致性协议机制实现的。

JMM的三个特性

原子性

一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。

基本类型数据的访问大都是原子操作,long 和double类型的变量是64位,但是在32位JVM中,32位的JVM会将64位数据的读写操作分为2次32位的读写操作来进行,这就导致了long、double类型的变量在32位虚拟机中是非原子操作,数据有可能会被破坏,也就意味着多个线程在并发访问的时候是线程非安全的。

实现原子性的手段:

1)使用synchronized

2)使用loke,即JDK提供的互斥锁

可见性

一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量的这种修改(变化)。

实现可见性的手段:

1)使用synchronized关键字,在同步方法/同步块开始时(Monitor Enter),使用共享变量时会从主内存中刷新变量值到工作内存中(即从主内存中读取最新值到线程私有的工作内存中),在同步方法/同步块结束时(Monitor Exit),会将工作内存中的变量值同步到主内存中去(即将线程私有的工作内存中的值写入到主内存进行同步)。

2)使用Lock接口的最常用的实现ReentrantLock(重入锁)来实现可见性:当我们在方法的开始位置执行lock.lock()方法,这和synchronized开始位置(Monitor Enter)有相同的语义,即使用共享变量时会从主内存中刷新变量值到工作内存中(即从主内存中读取最新值到线程私有的工作内存中),在方法的最后finally块里执行lock.unlock()方法,和synchronized结束位置(Monitor Exit)有相同的语义,即会将工作内存中的变量值同步到主内存中去(即将线程私有的工作内存中的值写入到主内存进行同步)。

3)final关键字的可见性是指:被final修饰的变量,在构造函数数一旦初始化完成,并且在构造函数中并没有把“this”的引用传递出去(“this”引用逃逸是很危险的,其他的线程很可能通过该引用访问到只“初始化一半”的对象),那么其他线程就可以看到final变量的值。

4)使用 volatile关键字,volatile的底层生成的是汇编lock指令,这个指令会要求强行写入主内存,并且可以忽略store buffer 这种缓存,从而达到可见性目的,而且会利用MESI协议,让其他缓存行失效。

有序性

对于一个线程的代码而言,我们总是以为代码的执行是从前往后的,依次执行的。这么说不能说完全不对,在单线程程序里,确实会这样执行;但是在多线程并发时,程序的执行就有可能出现乱序。用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。

实现有序性的手段:

Java提供了两个关键字volatile和synchronized来保证多线程之间操作的有序性,volatile关键字本身通过加入内存屏障来禁止指令的重排序,而synchronized关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现。

在单线程程序中,不会发生“指令重排”和“工作内存和主内存同步延迟”现象,只在多线程程序中出现。

原文链接:https://blog.csdn.net/qq_25409421/article/details/131273163

评论( 0 )

  • 博主 Mr Cai
  • 坐标 河南 信阳
  • 标签 Java、SpringBoot、消息中间件、Web、Code爱好者

文章目录