当前位置: 首页 > news >正文

JAVA多线程之JMM

文章目录

  • 1. Java内存模型
  • 2. 内存交互
  • 3. 三大特性
    • 3.1 可见性
      • 3.1.1 可见性问题
      • 3.1.2 原因
      • 3.1.3 解决方法
    • 3.2 原子性
    • 3.3 有序性
  • 4. 指令重排
  • 5. JMM 与 happens-before
    • 5.1 happens-before关系定义
    • 5.2 happens-before 关系

在继续学习JUC之前,我们现在这里介绍一下Java内存模型,也就是JMM,进而引出关键字volatile的使用条件。

1. Java内存模型

Java 内存模型是 Java Memory Model(JMM),本身是一种抽象的概念,实际上并不存在,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式

JMM 作用:

  • 屏蔽各种硬件和操作系统的内存访问差异,实现让 Java 程序在各种平台下都能达到一致的内存访问效果
  • 规定了线程和内存之间的一些关系

根据 JMM 的设计,系统存在一个主内存(Main Memory),Java 中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成

在这里插入图片描述

主内存和工作内存:

  • 主内存:计算机的内存,也就是经常提到的 8G 内存,16G 内存,存储所有共享变量的值
  • 工作内存:存储该线程使用到的共享变量在主内存的的值的副本拷贝

JVM 和 JMM 之间的关系:JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来:

  • 主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域
  • 从更低层次上说,主内存直接对应于物理硬件的内存,工作内存对应寄存器和高速缓存

2. 内存交互

Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作,每个操作都是原子

非原子协定:没有被 volatile 修饰的 long、double 外,默认按照两次 32 位的操作

在这里插入图片描述

  • lock:作用于主内存,将一个变量标识为被一个线程独占状态(对应 monitorenter)
  • unclock:作用于主内存,将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定(对应 monitorexit)
  • read:作用于主内存,把一个变量的值从主内存传输到工作内存中
  • load:作用于工作内存,在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
  • use:作用于工作内存,把工作内存中一个变量的值传递给执行引擎,每当遇到一个使用到变量的操作时都要使用该指令
  • assign:作用于工作内存,把从执行引擎接收到的一个值赋给工作内存的变量
  • store:作用于工作内存,把工作内存的一个变量的值传送到主内存中
  • write:作用于主内存,在 store 之后执行,把 store 得到的值放入主内存的变量中

3. 三大特性

3.1 可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

3.1.1 可见性问题

存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的。但是 final 修饰的变量是不可变的,就算有缓存,也不会存在不可见的问题。

如下面的代码所示:

@Slf4j(topic = "c.Test20")
public class Test1 {static boolean run = true;	//添加volatilepublic static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){// ....}});t.start();sleep(1);log.debug("我想停下t");run = false; // 线程t不会如预想的停下来}
}

可以看到,上面的代码在主线程的最后将 run 变量设置为了 false ,但是,线程t并没有像我们预期的一样停下来,这就是由于缓存存在的原因。

3.1.2 原因

我们可以分析下上面的流程:

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存
    在这里插入图片描述

  2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率。
    在这里插入图片描述

  3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。
    在这里插入图片描述

3.1.3 解决方法

  1. 使用volatile
    它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存,修改如下:

    @Slf4j(topic = "c.Test20")
    public class Test1 {volatile static boolean run = true;	//添加volatilepublic static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){// ....}});t.start();sleep(1);log.debug("我想停下t");run = false; // 线程t不会如预想的停下来}
    }
    
  2. 使用synchronized锁

    @Slf4j(topic = "c.Test20")
    public class Test1 {volatile static boolean run = true;	//添加volatilefinal static Object lock = new Object();public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){synchronized (lock){if(!run){break;}}}});t.start();sleep(1);log.debug("我想停下t");synchronized (lock){run = false;}}
    }
    

3.2 原子性

原子性:不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被分割,需要具体完成,要么同时成功,要么同时失败,保证指令不会受到线程上下文切换的影响

定义原子操作的使用规则:

  1. 不允许 read 和 load、store 和 write 操作之一单独出现,必须顺序执行,但是不要求连续
  2. 不允许一个线程丢弃 assign 操作,必须同步回主存
  3. 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步会主内存中
  4. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(assign 或者 load)的变量,即对一个变量实施 use 和 store 操作之前,必须先自行 assign 和 load 操作
  5. 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁,lock 和 unlock 必须成对出现
  6. 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新从主存加载
  7. 如果一个变量事先没有被 lock 操作锁定,则不允许执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量
  8. 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)

3.3 有序性

JAVA 运行时有的操作是无序的,无序是因为发生了指令重排序。

CPU 的基本工作是执行存储的指令序列,即程序,程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程,为了提高性能,编译器和处理器会对指令重排,一般分为以下三种:

源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令

比如下面的代码:

int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}
}
// 线程2 执行此方法
public void actor2(I_Result r) {num = 2;ready = true;
}

如果执行上面的两个线程,那么可能的结果有哪些呢?,很明显有下面两种:

  • 先执行线程1再执行线程2,那么结果是1
  • 先执行线程2再执行线程1,那么结果是4
  • 但是,除此之外,还有一种发生重排的情况,即线程2的指令重排为先执行 ready = true; 再执行 num = 2; ,这样的话,结果就是0

如果我们希望指令不进行重排,那么可以再定义变量时加一个volatile修饰变量 ready ,这样,可以使得ready代码之前的部分不发生重排,所以 num 就不用加 volatile修饰了。

4. 指令重排

上面我们提到了,有时候指令并不按照我们编写代码的顺序来进行执行,这是因为发生了指令重排,那么为什么要发生指令重排呢?不发生指令重排程序不是更加显明吗?

其实不是这样的,每一个指令都会包含多个步骤,每个步骤可能使用不同的硬件。因此,流水线技术产生了,它的原理是指令 1 还没有执行完,就可以开始执行指令 2,而不用等到指令 1 执行结束后再执行指令 2,这样就大大提高了效率,指令流水线如下图所示:
在这里插入图片描述

上述流水线技术能够很好的提高CPU的效率,但是,流水线术最害怕中断,恢复中断的代价是比较大的,所以我们要想尽办法不让流水线中断。指令重排就是减少中断的一种技术。
我们分析一下下面这段代码的执行情况:

a = b + c;
d = e - f ;

先加载 b、c(注意,有可能先加载 b,也有可能先加载 c),但是在执行 add(b,c) 的时候,需要等待 b、c 装载结束才能继续执行,也就是需要增加停顿,那么后面的指令(加载 e 和 f)也会有停顿,这就降低了计算机的执行效率。

为了减少停顿,我们可以在加载完 b 和 c 后把 e 和 f 也加载了,然后再去执行 add(b,c) ,这样做对程序(串行)是没有影响的,但却减少了停顿。换句话说,既然 add(b,c) 需要停顿,那还不如去做一些有意义的事情(加载 e 和 f)。

因此,指令重排对于提高 CPU 性能十分必要,但也带来了乱序的问题。

指令重排一般分为以下三种:

  • 编译器优化重排,编译器在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
  • 指令并行重排,现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。
  • 内存系统重排,由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。所以在多线程下,指令重排序可能会导致一些问题。

5. JMM 与 happens-before

5.1 happens-before关系定义

一方面,我们开发者需要 JMM 提供一个强大的内存模型来编写代码;另一方面,编译器和处理器希望 JMM 对它们的束缚越少越好,这样它们就可以尽可能多的做优化来提高性能,希望的是一个弱的内存模型。

JMM 考虑了这两种需求,并且找到了平衡点,对编译器和处理器来说,只要不改变程序的执行结果(单线程程序和正确同步了的多线程程序),编译器和处理器怎么优化都行。

对于我们开发者来说,JMM 提供了happens-before 规则(JSR-133 规范),满足了我们的诉求——简单易懂,并且提供了足够强的内存可⻅性保证。 换言之,我们开发者只要遵循 happens-before 规则,那么我们写的程序就能保证在 JMM 中具有强的内存可⻅性。

JMM 使用 happens-before 的概念来定制两个操作之间的执行顺序。这两个操作可以在一个线程内,也可以是不同的线程种。

happens-before 关系的定义如下:

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

总之,如果操作 A happens-before 操作 B,那么操作 A 在内存上所做的操作对操作 B 都是可⻅的,不管它们在不在一个线程。

5.2 happens-before 关系

在Java中,有以下天然的happens-before 关系:

  1. 程序次序规则 (Program Order Rule):一个线程内,逻辑上书写在前面的操作先行发生于书写在后面的操作 ,因为多个操作之间有先后依赖关系,则不允许对这些操作进行重排序

  2. 锁定规则 (Monitor Lock Rule):一个 unlock 操作先行发生于后面(时间的先后)对同一个锁的 lock 操作,所以线程解锁 m 之前对变量的写(解锁前会刷新到主内存中),对于接下来对 m 加锁的其它线程对该变量的读可见

  3. volatile 变量规则 (Volatile Variable Rule):对 volatile 变量的写操作先行发生于后面对这个变量的读

  4. 传递规则 (Transitivity):具有传递性,如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C

  5. 线程启动规则 (Thread Start Rule):Thread 对象的 start()方 法先行发生于此线程中的每一个操作

    static int x = 10;//线程 start 前对变量的写,对该线程开始后对该变量的读可见
    new Thread(()->{	System.out.println(x);	},"t1").start();
    
  6. 线程中断规则 (Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生

  7. 线程终止规则 (Thread Termination Rule):线程中所有的操作都先行发生于线程的终止检测,可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行

  8. 对象终结规则(Finaizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始

相关文章:

JAVA多线程之JMM

文章目录 1. Java内存模型2. 内存交互3. 三大特性3.1 可见性3.1.1 可见性问题3.1.2 原因3.1.3 解决方法 3.2 原子性3.3 有序性 4. 指令重排5. JMM 与 happens-before5.1 happens-before关系定义5.2 happens-before 关系 在继续学习JUC之前,我们现在这里介绍一下Java…...

Windows10 专业版 系统激活

Windows10 专业版 系统激活 参考: Windows10系统激活技巧 第一步:在电脑桌面,新建一个文本文档 第二步:打开文本文档,输入以下代码后,直接保存关闭文档 slmgr/skms kms.03k.org slmgr/ato 第三步&#xff1…...

C#使用LINQ和EF Core

在实际应用中,您可以使用 LINQ 查询 EF Core 来执行各种数据库操作。通过 LINQ,您可以轻松地过滤、排序、分组和连接数据。 要使用LINQ查询EF Core中的数据,您可以按照以下步骤进行操作: 首先,确保您已经安装了 Entit…...

数字人解决方案— SadTalker语音驱动图像生成视频原理与源码部署

简介 随着数字人物概念的兴起和生成技术的不断发展,将照片中的人物与音频输入进行同步变得越来越容易。然而,目前仍存在一些问题,比如头部运动不自然、面部表情扭曲以及图片和视频中人物面部的差异等。为了解决这些问题,来自西安…...

HTML5语法总结

文章目录 一.HTML基本框架二.标题标签三.段落标签四.换行与水平线标签五.文本格式化标签(加粗、倾斜、下划线、删除线)六.图像标签扩展:相对路径,绝对路径与在线网址 七.超链接标签八.音频标签九.视频标签十.列表标签十一.表格标签扩展:表格结构标签合并…...

在github下载的神经网络项目,如何运行?

github网页上可获取的信息 在github上面,有一个requirements.txt文件,该文件说明了项目要求的python解释器的模块。 - 此外,还有一个README.md文件,用来说明项目的运行环境以及其他的信息。例如python解释器的版本是3.7、PyTorc…...

spring boot学习第十四篇:使用AOP编程

一、基本介绍 1,什么是 AOP (1)AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。 (2)利用 AOP…...

凯特信安云签解决方案

联合解决方案 凯特信安基于《电子签名法》设计“云签服务方案”,应用人脸识别、电子签章签名云服务等技术,支持多个自然人、多个企业等签名,满足各种移动终端签署的应用场景。面向不动产登记、工改系统等社会公众服务系统,针对自然…...

【xr806开发板使用】连接wifi例程实现

##开发环境 win10 WSL ##1、环境配置 参考:https://aijishu.com/a/1060000000287513 首先下载安装wsl 和ubuntu https://docs.microsoft.com/zh-cn/windows/wsl/install (1)安装repo: 创建repo安装目录: mkdir ~/…...

停车管理系统asp.net+sqlserver

停车管理系统asp.netsqlserver 说明文档 运行前附加数据库.mdf(或sql生成数据库) 主要技术: 基于asp.net架构和sql server数据库, 功能模块: 停车管理系统asp.net sqlserver 用户功能有菜单列表 我的停车记录 专…...

新增多项功能,龙讯旷腾开源机器学习力场PWMLFF 2024.3版本上线

人工智能与传统计算机模拟结合是当今科学计算的一大趋势,机器学习力场作为其中的一个重要方向,能够显著提升分子动力学模拟的精度和效率。PWMLFF是一套由龙讯旷腾团队开发,在 GNU 许可下的开源软件包,用于快速生成媲美从头算分子动力学(AIMD&…...

Docker常用命令练习

文章目录 Docker常用命令练习1.docker 基础命令2.镜像命令3.保存镜像4.加载镜像5.容器命令6.环境变量7. --rm8. --networkhost Docker常用命令练习 1.docker 基础命令 安装docker yum install docker启动docker systemctl start docker关闭docker systemctl stop docker重…...

Kafka(十)安全

目录 Kafka安全1 安全协议1.1 PALINTEXT1.2 SSL1.2.1 生成服务端证书1.2.2 生成客户端证书1.2.3 修改配置listenersadvertised.listenerslistener.security.protocol.mapinter.broker.listener.namesecurity.inter.broker.protocolcontrol.plane.listener.name 1.3 SASL_PLAINT…...

流畅的 Python 第二版(GPT 重译)(四)

第二部分:函数作为对象 第七章:函数作为一等对象 我从未认为 Python 受到函数式语言的重大影响,无论人们说什么或想什么。我更熟悉命令式语言,如 C 和 Algol 68,尽管我将函数作为一等对象,但我并不认为 Py…...

windows docker

写在前面的废话 最近在学习riscv的软件相关内容,倒是有别人的sg2042机器可以通过ssh使用,但是用起来太不方便了,经常断掉,所以想着在自己的机器上跑一跑riscv的操作系统。最常见的有两种方法吧,第一个就是qemu&#xf…...

中国1km分辨率逐月地表太阳辐射均值数据集(1960-2022)

地表太阳辐射是地球系统的主要驱动因子,驱动着地球系统的能量、水和碳循环。它是地表水文、生态、农业等陆表过程模拟的重要驱动数据,也是太阳能利用的重要指标。发展长时间序列、高分辨率的地表太阳辐射数据集,对于地表过程研究、太阳能电厂…...

Android中内存泄漏的检测,解决方案以及示例

单例模式使用不当引起的内存泄漏 1. 什么是内存泄漏? 安卓内存泄漏是因为长生命周期的对象持有了短生命周期的引用 导致本应该本回收的内存无法回收,导致内存的占用越来越大,最终可能导致程序崩溃或者系统资源不足等问题。 在Android开发中,内存泄漏是一个常见的问题,…...

Android静默安装一(Root版)

近期开发上线一个常驻app,项目已上线,今天随笔记录一下静默安装相关内容。我分三篇静默安装(root版)、静默安装(无障碍版)、监听系统更新、卸载、安装。先说说我的项目需求:要求app一直运行&…...

【漏洞复现】2.Apache Log4j2远程代码执行漏洞(CVE-2021-44228)复现及分析

文章目录 1. 预备知识2. 漏洞复现2.1 漏洞介绍2.2 漏洞原理分析2.2.1 Log4j介绍2.2.2 JNDI介绍2.2.3 利用原理 2.3 漏洞复现2.3.1 使用docker搭建复现环境2.3.2 DNSLog验证2.3.3 JNDI注入反弹shell 2.4 漏洞修复2.4.1修改log4j2版本2.4.2 临时解决方案 1. 预备知识 Apache是一个…...

Simulink|局部遮荫下光伏组件多峰值PSO-MPPT控制

目录 主要内容 1.光伏电池工程数学模型的输出特性程序 2.普通扰动观察法进行MPPT 3.基于粒子群寻优的多峰输出特性 4.PSO_MPPT仿真模型 下载链接 主要内容 在实际的光伏发电系统中,由于环境多变等因素的影响,当局部出现被遮挡情况时光伏阵列…...

Vim 调用外部命令学习笔记

Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

大数据学习栈记——Neo4j的安装与使用

本文介绍图数据库Neofj的安装与使用,操作系统:Ubuntu24.04,Neofj版本:2025.04.0。 Apt安装 Neofj可以进行官网安装:Neo4j Deployment Center - Graph Database & Analytics 我这里安装是添加软件源的方法 最新版…...

C++初阶-list的底层

目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...

Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例

使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...

学校招生小程序源码介绍

基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...

Spring AI与Spring Modulith核心技术解析

Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...

tomcat入门

1 tomcat 是什么 apache开发的web服务器可以为java web程序提供运行环境tomcat是一款高效,稳定,易于使用的web服务器tomcathttp服务器Servlet服务器 2 tomcat 目录介绍 -bin #存放tomcat的脚本 -conf #存放tomcat的配置文件 ---catalina.policy #to…...

Kafka主题运维全指南:从基础配置到故障处理

#作者:张桐瑞 文章目录 主题日常管理1. 修改主题分区。2. 修改主题级别参数。3. 变更副本数。4. 修改主题限速。5.主题分区迁移。6. 常见主题错误处理常见错误1:主题删除失败。常见错误2:__consumer_offsets占用太多的磁盘。 主题日常管理 …...

macOS 终端智能代理检测

🧠 终端智能代理检测:自动判断是否需要设置代理访问 GitHub 在开发中,使用 GitHub 是非常常见的需求。但有时候我们会发现某些命令失败、插件无法更新,例如: fatal: unable to access https://github.com/ohmyzsh/oh…...

图解JavaScript原型:原型链及其分析 | JavaScript图解

​​ 忽略该图的细节(如内存地址值没有用二进制) 以下是对该图进一步的理解和总结 1. JS 对象概念的辨析 对象是什么:保存在堆中一块区域,同时在栈中有一块区域保存其在堆中的地址(也就是我们通常说的该变量指向谁&…...