【JUC系列-01】深入理解JMM内存模型的底层实现原理

一,深入理解JMM内存模型

1,什么是可见性

在谈jmm的内存模型之前,先了解一下并发并发编程的三大特性,分别是:可见性,原子性,有序性。可见性指的就是当一个线程修改某个变量的值之后,其他的线程可以立马感知到。

接下来看一个例子,看一个线程改变值之后,另一个线程能否立马感知到这个值被改变了。

public class JmmTest {private boolean flag = true;private int count = 0;public void refresh() {flag = false;System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);}public void load() {while (flag) {//TODO  业务逻辑count++;}System.out.println(Thread.currentThread().getName() + "跳出循环: count=" + count);}public static void main(String[] args) throws InterruptedException {JmmTest test = new JmmTest();// 线程threadA模拟数据加载场景Thread threadA = new Thread(() -> test.load(), "threadA");threadA.start();// 让threadA执行一会儿Thread.sleep(1000);// 线程threadB通过flag控制threadA的执行时间Thread threadB = new Thread(() -> test.refresh(), "threadB");threadB.start();}
}

可以发现以上操作,线程A先加载这个flag值,由于是true,因此一直处于while循环中空转,但是线程B随后修改了这个值,但是可以发现线程A是还在这个while循环中的,并没有跳出循环,其结果值如下:

threadB修改flag:false

也就是说,在一个正常的多线程之间的通信,是不能够直接的进行通信的,因此这就需要了解JMM的底层原理了

2,什么是JMM

Java Memory Model ,就是JMM的全称,意思是java内存模型。主要用于规范java虚拟机和计算机内存时如何协调工作的,规定了当一个线程改变某个共享变量值后,其他线程需要如何查看以及合适可以查看这个被改变的共享数据。

jmm的内存模型如下,java采用的是共享变量的模型方式,在创建一个共享变量之后,这些共享变量时存储在主内存中的,所有线程都能访问,但是每个线程需要操作这个变量时,需要先将这个值加载到每个线程的工作内存中,即每个线程都有对应栈帧,将这个值加入到局部变量表即可,就成为了共享变量的一个副本,随后线程A才能去修改这个值

在这里插入图片描述

而由于主内存中的变量都是共享变量,因此为了解决并发问题,在JMM内部又引入了八大原子操作

1,lock:作用于主内存的变量,把一个变量标记为一条线程独占状态
2,unlock:把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
3,read(读取):作用于主内存中,需要先对变量进行副本的拷贝,然后将变量值传输到工作内存中
4,load(载入):在工作内存中,需要对传输过来的副本变量进行一个获取,并且存入到工作内存中
5,use(使用): 需要将获取的变量传给执行引擎
6,assign(赋值):执行引擎会将这个收到的变量赋值给工作内存的变量
7,store(存储):修改这个传过来的副本之后,会将修改的值存储并送到主内存中
8,write(写入):会将这个存储的变量写回到主内存中,即修改主内存的值

如当一个线程去修改主内存中的共享变量的方式如下,比如说内存中的 x = 5 进行 +1 的操作如下图所示,首先线程A会read读取主内存中的x = 5的值,随后将读取到的值load载入到线程A的本地内存中,一般栈帧中存放变量的都是这个局部变量表,随后会通过use的指令使用这个变量,将这个值加入到cpu中,结果cpu内部的运算之后,此时 x = 6,会通过assign方式将这个结果值从cpu返回到本地内存中,随后将这个值返回到主内存中,并通过store的方式将这个值存储,最后将被修改的变量写回到主内存中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jHCWw6Sa-1692553785831)(img/1692079002392.png)]

同时在使用这八种原子操作时,需要满足以下的规则

  • 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

3,引入volatile

在了解完这个jmm内存模型之后,知道java线程之间是如何进行线程通信的,再回到这个 JmmTest 方法中,现在可以大胆的猜测一下,是不是因为线程B修改完值后,没有人去通知线程A?所以才导致值没有发生变化

因此接下来继续验证,就是直接在这个flag变量前面增加一个关键字 volatile

private volatile boolean flag = true;

其结果如下,可以得出结论,线程A跳出了循环,就是意味着线程A接收到了这个最新的值

threadB修改flag:false
threadA跳出循环: count=399766740

因此查阅了一些资料,以及看了一下hotspot里面关于这个volatile关键字的源码,可以发现这个关键字是通过一个JVM的内存屏障来实现的。

storeload();  //jvm内存屏障,在汇编指令中,对应着lock关键字

内存屏障可以禁止该指令与前面和后面的读写指令重排序,并且可以使其他线程中的本地内存中的该值直接失效,这样其他内存就需要去主内存中获取改值,就能拿到最新的值了。因此volatile是通过内存屏障的方式来实现数据的可见性和有序性的。

除了这个volatile关键字之外,另外像synchronized,lock等这些锁底层都是采用了这个内存屏障来实现,因此这些重量级锁肯定也是可以保证可见性和有序性的,同时由于是重量级操作,除了这两种之外,他们同时还能保证原子性。

除了内存屏障可以保证可见性之外,关键字final也是可以保证可见性的。总而言之能保证可见性的方式只有两种:一种是内存屏障,一种是上下文切换

4,cpu缓存架构

在cpu中,主要由寄存器,程序计数器,高速缓存,逻辑运算单元组成,高速缓存又分了三级缓存,分别是一级缓存、二级缓存和三级缓存,一级缓存中又分为两部分,一个用于存储指令,一个用于存储数据。在inter处理器中,一个cpu又分为两个处理器,因此会存在两个cpu共享一个三级缓存的情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1HofjptX-1692553785832)(img/1692234264579.png)]

使用高速缓存主要是减少等待内存的时间,提升CPU的计算能力

接下来根据这个缓存架构再举一个例子,现在有两个线程,分别是线程thread1和线程thread2,假设主内存中有一个值x=100,接下来两个线程同时去读这个100,线程1加对这个值加10,线程2对这个值加20,那么根据JMM的八大原子操作,此时线程1的CPU的值为110,线程2的CPU的值为120,最终会将这个值写回主内存中。

那么此时主内存就会出现两种情况,如果线程1先写回,线程2后写回,那么线程2会将线程1写回的值覆盖掉,此时;如果线程2先写回,线程后写回,那么线程会将线程2写回的值给覆盖掉,这就是经典的线程不安全问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PcaYNfBx-1692553785835)(img/1692234699580.png)]

造成这种原因的主要问题,是因为缓存不一致的问题。 即线程1的高速缓存的值和线程2的高速缓存的值不一致所导致的,因此为了解决这种缓存一致性的问题,主要有两种解决方式:嗅探机制、基于目录的机制

5,嗅探机制

再了解完这个导致数据不安全的原因是由于缓存不一致的问题,因此为了解决这个硬件层面的缓存一致性,最流行的还是使用这种嗅探机制

其工作原理如下:就是说如果存在多个缓存被共享的时候,如果有处理器修改了共享变量的值,那么必须传播到其他所有具有该变量的副本中,通过这种传播机制来防止系统违反缓存的一致性。就是说,数据的变更通知是通过总线来完成的。当其他缓存接收到这个通知信息之后,可以选择重新的在主内存中刷新数据,也可以直接让当前缓存中的值直接失效,具体是哪种做法,还得取决于使用哪种缓存一致性协议。

写失效:就是某个处理器将值改完之后,直接通知其他处理器,让其他处理器的缓存值失效

写更新:就是处理器将值修改完之后,在通知其他处理器的时候,直接将值携带上,让其他的处理器缓存值更新

总线的带宽是有效的,因此写失效的使用范围是最广的。MSI、MESI、MOSI、MOESI等是最常见的缓存一致性协议

6,解决缓存一致性的MESI

为了解决缓存一致性,使用最多的方式是这种MESI的方式,总共有四种状态,分别是

  • M:modify,修改状态
  • E:Exclusive,独占状态
  • S:Share,共享状态
  • I:Invalid,失效状态

在这里插入图片描述

当工作内存将主内存的值加载到高速缓存之后,假设此时只有当前线程thread1加载了X=5,那么此时X是一个Exclusive独占状态,如果此时线程thread2也加载了这个值,那么此时该值则会从一个独占状态变成一个Share共享状态,如果此时线程thread1要修改这个值,那么在修改这个值后,X就会从一个共享状态变为一个Modify修改状态,并且在回显的时候被总线窥探到,总线就会发起请求告诉其他的线程这个被修改的值,让其他的线程缓存里面的改值直接失效Invalid,那么其他线程就可以去获取最新的值。

但是该协议并不是会直接生效,而是需要在特定的时候生效,就是需要一个lock前缀指令才可以满足该协议,如一些常见的volatile,synchronized,lock等关键字。这样才能解决这种缓存一致性的问题。但是volatile并不能保证原子性。

并且在某个线程更新了某个值之后,刷新主内存的线程会立即执行,这样才能让其他已经处于失效的线程立马的回到主内存中去更新改值,从而线程在获取值时减少数据的脏读问题以及长时间等待的问题。

除了缓存一致性协议之外,还有总线一致性协议,由于总线一致性的性能问题,缓存一致性协议才得以出现。

7,JMM内存可见性的保证

在单线程中:由于需要保证 else-if-serial 规范,即不管如何进行指令重排,都必须要保证最终结果的一致性,因此,单线程不存在内存可见性的问题,不管是编译器还是及时处理器等,都必须保证和原始顺序所执行的结果值相同

在正确同步的多线程中:如在加锁的情况下,JMM在内部会禁止指令重排的操作,并且在底层会通过内存屏障的操作来操作底层硬件,从而实现可见性和有序性的操作。

未同步的多线程:JMM不能保证未同步的执行结果与顺序一致性的结果一致。由于在JVM中,存在一些JIT即时编译器以及解释器的一些优化等,因此就会出现指令重排的情况。

x = 10;						y = 100;													
y = 100;          ====>		x = 10;
z = x + 10;					z = x + 10;

举个例子,如在单例模式加锁的双重检测中,需要在对象的前面加一个关键字 volatile,如果不加的话,在new对象的时候,会经历以下步骤:开辟内存空间,堆内存初始化,栈中对象指向堆中对象。这里就会出现一个问题,由于new对象并没有保证这个原子操作,因此就会出现指令重排的情况,就是可能会先指向堆中的对象,再在堆内存中初始化,就是第二步和第三步的顺序可能会发生改变。

public class SingletonTest{private volatile static SingletonTest instance = null;private SingletonTest() {}public static SingletonTest getInstance() {if (instance == null) {synchronized (SingletonTest.class) {if (instance == null) {//在不加volatile或者其他锁的情况下//可能会出现指令重排的情况instance = new Singleton();}}}return instance;}
}

那在多线程的情况下,在第一个线程正好执行到发生指令重排的第二步,就是指向了一个堆中的对象,但还没有初始化,只是经历了实例化,而第二个线程进行第一个if判断的时候,此时并没有加锁,所以发现不为null,就直接return了,但是return的是一个你有进行初始化的一个值,因此返回的对象肯定是有问题的

所以为了解决这个指令重排的问题,就需要在这个对象上面加上volatile这个关键字了,这样就能禁止指令重排了

private volatile static SingletonTest instance = null;

8,内存屏障

在jvm和硬件层面都有实现内存屏障的方式。

在jvm层面,在JSR规范中定义了四种内存屏障,分别是LoadStore,LoadLoad,StoreLoad,StoreStore。Load操作可以当做成是一个read读取操作,Store操作可以当做成是一个写入操作,两个操作之间相当于加了一个一堵墙,从而保证两个操作的顺序不被打乱

LoadStore:在store2指令写入数据之前,保证数据一定被load1指令先写入进去

LoadLoad:在Load2指令读取数据之前,保证数据一定被load1指令先读取出来

StoreLoad:在Load2指令读取数据之前,保证数据一定被Store指令写入进去

StoreStore:在store2指令写入数据之前,保证数据一定被load1指令读取出来

并且以上的写入操作,都是可以实现所有的处理器都可以感知到数据的变化,即保证可见性。当前jvm底层实现内存屏障的方式主要是通过这个StoreLoad方式来实现的。

在硬件层面,也提供了一系列的内存屏障的方式保证数据的一致性,主要是通过ifence和sfence来实现读写屏障,也可以通过Lock前缀来实现这个类似于内存屏障的功能。但是在JMM内存模型中屏蔽了这种底层硬件带来的差异,直接由JVM来为不同的平台生成相应的字节码。

9,为何多线程的累加值总是小于期待值

了解这个JMM的内存模型之后,接下来通过之前的多线程的系列的文章,来对上述这个问题做一个初步的了解。

count++;

由于在java中,实现线程的方式是使用的内核态的方式实现的多线程,也就是说开发者只能通过内核去调用操作系统,再去调用线程,因此开发人员并不能控制线程,因此就不能控制上下文切换等,并且实现线程的方式是抢占式的方式实现,所以在累加操作中,某个值可能只执行了一半,就出现了cpu中时间片的切换,导致这个值被其他线程操作,如果是在多线程的情况下,两个线程同时操作一个值,就会出现这种值被覆盖的问题。因此最终出现的结果会小于期待值

其次是通过JMM模型可知,每个线程都有属于自己的工作区间,但是每个线程在将值修改之后,其他线程并不能感知到,就是无法保证可见性的问题,因此也会出现大量的值被覆盖。所以累加的结构也会小于期待值

因此需要通过加锁的方式强行保证线程间执行顺序,以及需要通过实现内存屏障的方式来实现线程间的可见性和有序性以及原子性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/46037.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

卷积神经网络全解:(AlexNet/VGG/ GoogLeNet/LeNet/ResNet/卷积/激活/池化/全连接)、现代卷积神经网络、经典卷积神经网络

CNN,卷积神经网络,Convolution Neural Network 卷积计算公式:N (W-F2p)/s1 这个公式每次都得看看,不能忘 1 经典网络 按照时间顺序 1.1 LeNet LeNet是 Yann LeCun在1998年提出,用于解决手…

Lua 数据结构

一、Lua 中的数据结构 Lua 中并没有像 java、kotlin 语言有专门的数组、列表、集合等数据结构的类,而只提供 table 类型,但他很强大而且也很灵活,而且也在很多场景中天然的就解决了如何对接和使用的问题(例如模块的引入&#xff…

MYSQL完全卸载、安装与账号创建、权限控制

一、卸载mysql CentOS 卸载 MySQL 1. 查看安装情况 使用以下命令查看当前安装mysql情况,查找以前是否装有mysql rpm -qa|grep -i mysql这里显示我安装的 MySQL 服务有有: 2. 停止 mysql 服务、删除之前安装的 mysql 删除命令:rpm -e –n…

C++笔记之条件变量(Condition Variable)与cv.wait 和 cv.wait_for的使用

C笔记之条件变量(Condition Variable)与cv.wait 和 cv.wait_for的使用 参考博客:C笔记之各种sleep方法总结 code review! 文章目录 C笔记之条件变量(Condition Variable)与cv.wait 和 cv.wait_for的使用1.条件变量&…

v8引擎编译全过程

环境vs2019 cmd 命令行需要设置成为代理模式 set http_proxyhttp://127.0.0.1:10809 set https_proxyhttp://127.0.0.1:10809 这个必须带上,不然报错,告诉编译器win系统的模式 set DEPOT_TOOLS_WIN_TOOLCHAIN0 源码 GitHub: GitHub - v8/v8: The…

Eclipse如何设置快捷键

在eclopse设置注释行和取消注释行 // 打开eclipse,依次打开:Window -> Preferences -> General -> Key,

solidwords(6)

从右视图开始,分上下两部分 标题 这里的薄壁要留意一下怎么算的(单向:默认向内;如果想向外记得选反向)

【Spring专题】Spring之Bean的生命周期源码解析——阶段二(二)(IOC之属性填充/依赖注入)

目录 前言阅读准备阅读指引阅读建议 课程内容一、依赖注入方式(前置知识)1.1 手动注入1.2 自动注入1.2.1 XML的autowire自动注入1.2.1.1 byType:按照类型进行注入1.2.1.2 byName:按照名称进行注入1.2.1.3 constructor:…

idea 新建servlet 访问提示404 WebServlet注解找不到包 报错

检查访问路径是否设置正确 如果设置为name “/testServlet”,则会404 WebServlet注解报错找不到包 检查是否引入了tomcat依赖包

线性代数的学习和整理8: 方阵和行列式相关(草稿-----未完成)

1.4.1 方阵 矩阵里,行数列数的矩阵叫做方阵方阵有很多很好的特殊属性 1.4.2 行列式 行列式是方阵的一种特殊运算如果矩阵行数列数相等,那么这个矩阵是方阵。行列数的计算方式和矩阵的不同只有方阵才有行列式行列式其实是,矩阵变化的一个面…

超越函数界限:探索JavaScript函数的无限可能

🎬 岸边的风:个人主页 🔥 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想,就是为了理想的生活 ! 目录 📚 前言 📘 1. 函数的基本概念 📟 1.1 函数的定义和调用 📟 1.2 …

动态内存管理

目录 为什么要用动态内存开辟 动态内存有关函数 void* malloc (size_t size); void free (void* ptr); void* calloc (size_t num, size_t size); void* realloc (void* ptr, size_t size); C/C程序的内存开辟 柔性数组 特点: 柔性数组的使用: 为什么要用…

【nodejs】用Node.js实现简单的壁纸网站爬虫

1. 简介 在这个博客中,我们将学习如何使用Node.js编写一个简单的爬虫来从壁纸网站获取图片并将其下载到本地。我们将使用Axios和Cheerio库来处理HTTP请求和HTML解析。 2. 设置项目 首先,确保你已经安装了Node.js环境。然后,我们将创建一个…

学习笔记|基于Delay实现的LED闪烁|u16是什么|a--和--a的区别|STC32G单片机视频开发教程(冲哥)|第六集(上):实现LED闪烁

文章目录 摘要软件更新什么是闪烁Tips:u16是什么? 语法分析:验证代码Tips:a--和--a的区别(--ms 的用法)测试代码: 摘要 1.基于Delay实现的LED闪烁 2.函数的使用 3,新建文件,使用模块化编程 软件更新 打…

macOS(m1/m2)破解Sublime Text和Navicat16

破解Sublime Text 说明:全程使用的是终端操作 1. 下载Sublime Text,建议使用brew下载 2. 进入到下载的app的文件夹 cd "/Applications/Sublime Text.app/Contents/MacOS/"3. 执行以下操作以确认版本是否匹配 md5 -q sublime_text | grep -i…

消息中间件的选择:RabbitMQ是一个明智的选择

💗wei_shuo的个人主页 💫wei_shuo的学习社区 🌐Hello World ! MQ(Message Queue) MQ(消息队列)是一种用于在应用程序之间进行异步通信的技术;允许应用程序通过发送和接收…

css学习3(三种样式表与样式控制优先级)

1、外部样式表&#xff1a;当样式需要应用于很多页面时&#xff0c;外部样式表将是理想的选择。在使用外部样式表的情况下&#xff0c;你可以通过改变一个文件来改变整个站点的外观。每个页面使用 <link> 标签链接到样式表&#xff0c;也要放到<head>中。 2、外部…

上网课用什么耳机和麦克风,分享几款骨传导耳机上网课用

各位耳机狂热者&#xff0c;咱们都了解传统的蓝牙耳机相对于老式有线耳机来说确实方便得多。但是&#xff0c;也别忘了蓝牙耳机会导致耳道不断堵塞&#xff0c;引发细菌滋生等问题。好在近年来&#xff0c;骨传导耳机如火如荼地走红&#xff0c;解决了这些难题&#xff0c;简直…

飞天使-jenkins进行远程linux机器修改某个文件的思路

文章目录 jenkins配置的方式jenkins中执行shell的思路 jenkins配置的方式 jenkins中执行shell的思路 下面的脚本别照抄&#xff0c;只是一个思路 ipall"$ips"# 将文本参数按行输出为变量 while IFS read -r line; doecho "$line" if [[ ! -z $line ]] &…

ubuntu 22.04 LTS 在 llvm release/17.x 分支上编译 cookbook llvm example Chapter 02

不错的资料&#xff1a; LLVMClang编译器链接器--保值【进阶之路二】 - 掘金 —————————————————————————————————————— 下载 llvm-cookbook example: $ git clone https://github.com/elongbug/llvm-cookbook.git 也可以参照llvm-pr…