JUC JMM Java 内存模型

news2025/1/20 3:35:17

文章目录

  • 计算机存储系统
    • 高速缓冲存储器一致性
  • JMM(Java Memory Model)
    • 可见性
    • 原子性
    • 有序性
      • 指令重排
      • Happens-Before 原则
  • volatile 关键字
    • volatile 保证可见性
    • volatile 不能保证原子性
    • volatile 禁用指令重排(保证有序性)
    • 内存屏障(Memory Barrier)

计算机存储系统

存储系统是计算机的重要组成部分之一。存储系统提供写入和读出计算机工作需要的信息(程序和数据)的能力,实现计算机的信息记忆功能。现代计算机系统中常采用寄存器、高速缓存、主存、外存的多级存储体系结构。借用一张网络图片如下:

在这里插入图片描述

如图:其中越顶端的越靠近CPU,存储器的速度越快、容量越小、相应的价格越高。

  • CPU 寄存器:寄存器是 CPU 的组成部分,是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。(在 JVM 中,寄存器是线程私有的,每个线程使用的局部变量是存储在寄存器中,且相互不影响)
  • 高速缓存(L1、L2、L3)
    在这里插入图片描述
    • L1 缓存分成两种,一种是指令缓存,一种是数据缓存。L2 缓存和 L3 缓存不分指令和数据。
    • L1 和 L2 缓存在每一个 CPU 核中,L3 则是所有 CPU 核心共享的内存。
    • L1、L2、L3 的越离CPU近就越小,速度也越快,越离 CPU 远,速度也越慢。

再往后面就是内存,内存的后面就是硬盘以及一些外接存储设备等。这些设备的存储速度:

  • L1 的存取速度:4 个CPU时钟周期
  • L2 的存取速度:11 个CPU时钟周期
  • L3 的存取速度:39 个CPU时钟周期
  • RAM内存的存取速度 :107 个CPU时钟周期

打开我们电脑的任务管理器,选择 CPU 就能查看 L1、L2、L3 缓存的大小
在这里插入图片描述

我们编写的 Java 代码中的多线程共享变量(就是存储在堆内存的变量:如 对象的字段、static 静态变量等都是多线程共享),数据就从内存向上,先到 L3,再到 L2,再到 L1,最后到寄存器进行 CPU 计算。由于每个存储设备效率不一样,比如寄存器已经进行了 10 次计算,L1 缓存才存取一次数据,这就会导致一个比较复杂的问题:缓存一致性问题。

高速缓冲存储器一致性

达到如下情况表示缓存是一致的

  1. 任何进程所发出的缓存操作被存储器所观察到的顺序必须与进程发出操作的顺序相同;
  2. 每个读操作所返回的值必须是最后一次对该存储位置的写操作的值。

如果不满足以上情况,在每次运算之后,不同的进程可能会看到不同的值,这就是缓存一致性问题。

说了大半天,终于要绕回我们 Java 了

JMM(Java Memory Model)

JMM是一种抽象的概念,它规定了Java 程序中多线程并发访问共享内存的方式和规则,保证了多线程程序在不同平台上的正确性和一致性。

JMM 只是一个规范,JMM 要实现屏蔽各种硬件和操作系统的访问差异,需要依托于 JVM 。所以我们可以认为 JMM 是 JVM 的一部分。

可见性

指当一个线程修改了某一个共享变量的值,其他线程是否立即知道该变更,JMM 规定了所有的变量都在主内存中

在这里插入图片描述

  1. 共享变量存储在主内存
  2. 每个线程有其私有的工作内存
  3. 线程执行时,从主内存读取共享变量的副本到工作内存进行计算(Load)
  4. 线程计算完成后,将计算后的副本写回主内存(Save)
  5. 线程间的通信是通过这些副本来实现的(但由于这些线程读写的时机并不相同,就可能出现数据不一致的情况)

如果一个共享变量初始值为 a = 10,有 A、B、C 三个线程并发执行,假设A、B、C 线程依次修改完成 a 变量,其操作示意图如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这样线程 A、B 对 a 变量的操作就被覆盖了
可见性的代码示例:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class JMMTest {

    private static boolean flag = true;

    public static void main(String[] args) {

        new Thread(()->{

            while (true){
                 // 如果这里添加打印,flag 也会对该线程可见
                 // 因为 println 方法中有 Synchronized 代码块
                // System.out.println();
                if(!flag){
                    break;
                }
            }

        },"t1").start();


        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.debug("修改前");
        flag = false;
        log.debug("修改后");

    }

}
  1. 可以通过 volatile 关键字来解决此问题(volatile 关键字只能解决可见性问题,并不保证原子性和有序性。)
  2. 也可以在线程中添加 synchronized 关键字为其加锁也可以达到目的( synchronized 关键字既可保证可见性,也可以保证原子性,但 synchronized 关键字是重量级锁)
  3. synchronized 关键字不能阻止指令重排,但它保证了只有一个线程能操作 synchronized 块中的共享变量,如果这个共享变量不会出现在 synchronized 块外,被其他线程使用, synchronized 关键字也可以保证有序性的结果

原子性

是指同一个操作不可打断,在多线程情况下,操作不能被其他线程锁干扰。(类比数据库的事务的原子性)

有序性

有序性保证了一个线程在执行过程中,每个操作都按照一定的顺序进行。它通过禁止指令重排和设置内存屏障来实现有序性。

指令重排

指令重排是指编译器在不改变程序执行结果的前提下,对指令进行重新排列,以提高性能。也就是说我们写的代码是 1234 行,但指令重排后真正执行的顺序为:3421。但此过程遵循指令之间的依赖关系,比如第 1 行为变量定义,第二行为变量的使用,那么第二行就不能被重排为第一行。

在单线程情况下,指令重排可以提高性能,且没有问题出现,但多线程情况下可能会导致程序出现错误。所以在多线程情况下,我们会采用禁止指令重排和设置内存屏障的方法来处理这类问题。

Happens-Before 原则

指一个线程的写操作,对另一个线程的读操作可见,那么这两个线程之间遵循 Happens-Before 原则。

  • 次序性规则:一个线程内,写在前面的操作先于写在后面的操作执行。
  • 锁定规则:一个锁的 unlok 先于 lock 方法执行(就是同一个锁必须解锁后才能上锁)
  • volatile变量规则:对 volatile 变量的写操作先于读操作执行
  • 传递性规则:如果 A 线程先于 B 线程执行,B 线程先于 C 线程执行,那么 A 线程会先于 C 线程执行。
  • 线程启动规则:线程的 start 方法先于线程内容执行。
  • 线程中断规则:线程中断发生后才能检测到中断
  • 线程终止规则:线程中所有操作都先于线程终止执行
  • 线程终结规则:一个对象的初始化方法先于其 finalize 方法执行

总结起来就是 Happens-Before 原则就是程序执行时,必须遵循一定的客观顺序(就是要吃饭就得先张嘴,客观的顺序,不能违背客观规律)。

volatile 关键字

如我们在 JMM 可见性中的示例,volatile 关键字可解决可见性问题,用于保证某个共享变量被某个线程操作后,其操作后的结果对其他线程立即可见(重新从主内存获取更新后的值到副本),我们在上面的 JMM 可见性中的示例就是 volatile 保证可见性的一个示例

volatile 保证可见性

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class VolatileTest1 {

    private static volatile boolean flag = true;

    public static void main(String[] args) {

        new Thread(()->{

            while (true){
                // 如果这里添加打印,flag 也会对该线程可见
                // System.out.println();
                if(!flag){
                    break;
                }

            }

        },"t1").start();


        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.debug("修改前");
        flag = false;
        log.debug("修改后");

    }

}

volatile 不能保证原子性

volatile 关键字不能保证原子性,也就是说 volatile 无法达到替换锁(Lock、Synchronized)的目的。但如果只有一个线程写,其他线程都是读的情况下,volatile 关键字可以达到我们想要的效果。所以我们常用 volatile 来保证共享数据对其他线程可见,用锁来保证共享数据到写的一致性(原子性)。

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class VolatileTest1 {


    public static void main(String[] args) {
        Container1 c1 = new Container1();
        Container2 c2 = new Container2();

        for(int i = 0; i < 10; i++) {
            new Thread(() -> {
                for(int j = 0; j < 1000; j++){
                    c1.add();
                    c2.add();
                }
            }, "t"+i).start();
        }


        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(c1.getNum());
        System.out.println(c2.getNum());

    }

}

@Getter
class Container1{
    private int num;

    public synchronized void add(){
        num++;
    }
}

@Getter
class Container2{
    private volatile int num;

    public void add(){
        num++;
    }
}

运行结果:

10000
9859 //<-- 此值不确定,但肯定比10000小(原因就是 Volatile 不能保证原子性)

volatile 禁用指令重排(保证有序性)

class Test2Container{
    private int num;
    private boolean flag;

    public void write(){
        num = 10;
        flag = true; // 如果指令重排,此指令可能在 num = 10 之前执行
    }

    public void read(){
        if(flag){
            System.out.println(num);// 如果指令重排,此处可能出现 0 
        }
    }

}

使用 volatile 禁用指令重排

@Getter
class Test2Container{
    private int num;
    private volatile boolean flag;

    public void write(){
        num = 10;
        flag = true; // Volatile 写之后加入写屏障,
        // 写内存屏障会确保在此次写操作之后所有的读操作都会被更新,从而保证其他线程能够立即感知到修改的值。
    }

    public void read(){
        // 读内存屏障会确保在此次读操作之前所有的写入操作都会被完成,从而保证读取到的值是最新的
        if(flag){ // // Volatile 读之前加入读屏障
            System.out.println(num);
        }
    }

}

内存屏障(Memory Barrier)

读写屏障的实现机制是通过在指令之间插入内存屏障(Memory Barrier)来禁止指令重排。内存屏障是一种同步机制,它会强制让编译器和处理器按照指定的顺序执行指令。

  • 写屏障

对 Volatile 变量的写指令后会加入写屏障,写内存屏障会确保在此次写操作之后所有的读操作都会被更新,从而保证其他线程能够立即感知到修改的值。

  • 读屏障

对 Volatile 变量的读指令前会加入读屏障,读内存屏障会确保在此次读操作之前所有的写入操作都会被完成,从而保证读取到的值是最新的

其实还有一种 4 种屏障的说法,但它们的理解上晦涩难懂,不如读写屏障这样解释得清楚。如果非要较这个真,可以自行搜索搜索。

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

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

相关文章

Unity 代码控制Text自适应文本高度

在使用代码给Text赋值时&#xff0c;且文本有多段&#xff0c;并需要根据实际文本高度适配Text组件的高度时&#xff0c;可以使用以下方法&#xff1a; //Text文本 public TextMeshProUGUI text;void Start() {//代码赋值文本text.text "好!\n很好!\n非常好!";//获…

Spring系列学习三、Spring的基础组件

Spring的基础组件 一、 什么是Bean及Bean的生命周期二、 什么是依赖注入(DI)与控制反转(IOC)三、 Spring的作用域与生命周期四、 Spring的配置方式(XML与基于注解的配置)五、HelloWorld示例六、结语 欢迎回来各位亲爱的小伙伴&#xff01;我们已经滑过Spring的门槛&#xff0c;…

Unity中URP下的添加雾效支持

文章目录 前言一、URP下Shader支持雾效的步骤1、添加雾效变体2、在Varying结构体中添加雾效因子3、在顶点着色器中&#xff0c;我们使用内置函数得到雾效因子4、在片元着色器中&#xff0c;把输出颜色 和 雾效因子混合输出 二、在Unity中打开雾效三、测试代码 前言 我们使用之…

mapboxgl 中热力图的实现以及给热力图点增加鼠标移上 popup 效果

文章目录 概要效果预览技术思路技术细节小结 概要 本篇文章还是关于最近做到的 mapboxgl 地图展开的。 借鉴官方示例&#xff1a;https://iclient.supermap.io/examples/mapboxgl/editor.html#heatMapLayer 效果预览 技术思路 将接口数据渲染到地图中形成热力图。还需要将热…

机器学习深度学习面试笔记

机器学习&深度学习面试笔记 机器学习Q. 在线性回归中&#xff0c;如果自变量之间存在多重共线性&#xff0c;会导致什么问题&#xff1f;如何检测和处理多重共线性&#xff1f;Q. 什么是岭回归(Ridge Regression)和Lasso回归(Lasso Regression)&#xff1f;它们与普通线性回…

西北大学844计算机类考研-25级初试高分总攻略

西北大学844计算机类考研-25级初试高分攻略 个人介绍 ​ 本人是西北大学22级软件工程研究生&#xff0c;考研专业课129分&#xff0c;过去一年里在各大辅导机构任职&#xff0c;辅导考研学生专业课844&#xff0c;辅导总时长达400小时&#xff0c;辅导学生超过20余人&#xf…

交换域系数的选择:图像处理与编码的关键策略

在图像处理和编码领域&#xff0c;选择适当的交换域系数对于实现高效的图像处理和编码至关重要。交换域系数是指在特定的数学变换下产生的频域系数。通过选择合适的交换域系数&#xff0c;可以实现图像的压缩、增强和重构。本文将深入探讨交换域系数的选择在图像处理和编码中的…

中小企业运营难题:CRM系统为您解决!

​CRM如何帮助中小企业解决业务与团队之间的问题&#xff1f;它可以帮助企业获取潜在客户、数据储存管理、建立标准化流程、减少客户流失、促进客户沟通等。客户关系管理的核心理念要以客户为最主要的资源&#xff0c;提供全面的客户服务&#xff0c;满足客户要求&#xff0c;实…

如何从huggingface上下载模型

现在通过git 的方式已经几乎下载不到huggingface上的东西了。不过我们还可以使用huggingface-cli实现快速下载&#xff0c;以openlm-research/open_llama_3b_v2为例。 首先安装huggingface-cli pip install -U huggingface_hub 接下来使用&#xff1a; huggingface-cli downloa…

2023年华为OD机试(python)B卷-符合要求的结对方式

一、题目 题目描述&#xff1a; 用一个数组A代表程序员的工作能力&#xff0c;公司想通过结对编程的方式提高员工的能力&#xff0c;假设结对后的能力为两个员工的能力之和&#xff0c;求一共有多少种结对方式使结对后能力为N。 二、输入输出 输入描述: 5 1 2 2 2 3 4 第一行为…

丰田「退股」电装,传统汽车供应链体系走到十字路口

就在中国市场热衷于车企与零部件厂商的合纵连横之际&#xff0c;本周&#xff0c;传统汽车巨头丰田公司宣布&#xff0c;将出售部分电装&#xff08;Denso&#xff09;公司股份&#xff0c;资金用于投入电动化、智能驾驶等新技术研发。 按照计划&#xff0c;丰田汽车拟出售超过…

python之Selenium WebDriver安装与使用

首先把python下载安装后&#xff0c;再添加到环境变量中&#xff0c;再打开控制台输入: pip install selenium 正常情况下是安装好的&#xff0c;检查一下“pip show selenium”命令&#xff0c;出现版本号就说明安装好了。 1&#xff1a;如果出现安装错误&#xff1a; 那就用“…

2024年HTML+CSS+JS 网页版烟花代码

&#x1f482; 个人网站:【 海拥】【神级代码资源网站】【办公神器】&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交流的小伙伴&#xff0c;请点击【全栈技术交流群】 直接跳到末尾 获取完整源码 在线体验地址&…

【docker实战】02 用docker安装mysql

本示例采用bitnami的镜像进行安装MySQL 一、镜像搜索 先搜索一下mysql有哪些镜像 [rootlocalhost ~]# docker search mysql NAME DESCRIPTION STARS OFFICIAL AUTOMATED mysql …

操作系统【设备管理】

设备管理 一、前言 学习了存储器管理后&#xff0c;继续学习设备管理&#xff0c;设备管理的主要功能有缓冲区管理、设备分配、设备处理、虚拟设备及实现设备独立性等&#xff0c;由于I/O设备不仅种类繁多&#xff0c;而且他们的特性和操作方式往往相差甚大&#xff0c;使得设…

leetcode贪心算法题总结(一)

此系列分三章来记录leetcode的有关贪心算法题解&#xff0c;题目我都会给出具体实现代码&#xff0c;如果看不懂的可以后台私信我。 本章目录 1.柠檬水找零2.将数组和减半的最少操作次数3.最大数4.摆动序列5.最长递增子序列6.递增的三元子序列7.最长连续递增序列8.买卖股票的最…

SpringBoot发布项目到docker

Dockerfile FROM openjdk:11 # 作者 MAINTAINER chenxiaodong<2774398338qq.com># 安装 vim # RUN yum -y install vim# 环境变量 # 进入容器后的默认工作目录 ENV WORKPATH /usr/local/webapp ENV EXECFILE Docker2Application-0.0.1-SNAPSHOT.jarRUN mkdir -p $WORKPA…

数据探查系列:如何进行有意义的探索性数据分析(EDA)

如何进行有意义的探索性数据分析&#xff08;EDA&#xff09; 目录 1. 设置 1.1 导入库1.2 导入数据1.3 数据集特征1.4 数据集属性 2. 探索训练集和测试集 2.1 训练集 - 快速概览2.2 训练集 - 基本统计2.3 测试集 - 快速概览2.4 测试集 - 基本统计 3. 特征分布4. 数据不平衡检查…

【OpenCV】告别人工目检:深度学习技术引领工业品缺陷检测新时代

目录 前言 机器视觉 缺陷检测 工业上常见缺陷检测方法 内容简介 作者简介 目录 读者对象 如何阅读本书 获取方式 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。 点击跳转到网站 机器视觉…

深入探索MongoDB集群模式:从高可用复制集

MongoDB复制集概述 MongoDB复制集主要用于实现服务的高可用性&#xff0c;与Redis中的哨兵模式相似。它的核心作用是数据的备份和故障转移。 复制集的主要功能 数据复制&#xff1a;数据写入主节点&#xff08;Primary&#xff09;时&#xff0c;自动复制到一个或多个副本节…