内存可见性问题

news2024/11/22 19:00:14

目录

1.什么是内存可见性问题

2.内存可见性问题是怎么发生的

3.解决方法:volatile

4.volatile使用的注意事项

5.内存可见性问题的延伸

缓存(cache)


1.什么是内存可见性问题

首先来看一段代码

class Counter{
    public int flag = 0;
}
public class VolatileDemo1 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while(counter.flag == 0) {
                //循环里面不进行任何操作
            }
            System.out.println("t1 循环结束");
        });


        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入flag: ");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这段代码一共创建了两个线程,其中t1线程去判断flag的值(默认为0),如果不为0则跳出循环(循环里面不执行任何操作)当flag不为0时,提示t1线程结束,t2线程则是输入一个值,赋给flag。

按照我们的逻辑,当t2线程输入一个不为0的数字时,t1线程会打印“t1 循环结束”,那么我们来看一下结果,如下:

 可以看到,我们输入1,赋值给flag,但是t1循环却没有对此做出相应的操作,这就是出现了内存可见性问题。

2.内存可见性问题是怎么发生的

首先针对上面的例子,我们做一些分析。

t1线程中有一个循环,循环条件是判断flag这个变量是否为0,循环体为空

t2线程是输入一个数字赋值给flag

按照逻辑当t2输入数字不为0,那么t1循环结束,那么为什么当t2输入了一个不为0的数字时,t1循环仍然没有结束呢?

可以肯定的是:t2中的输入和赋值操作都是没有问题的,那么问题的所在就一个在t1的身上。

那么我们对t1中的执行语句做一些分析:

t1线程中储粮打印操作,唯一可以被执行的计算循环的判断条件 counter.flag == 0 。

这条语句我们可以把它拆分成两条指令:

一条是从内存中获取flag的值--load

一条是将这个值和0进行比较--cmp

按理来说,如果每次进入循环条件判断的时候,都对flag的值进行获取,那么结果就不会出现死循环的现象,而此时出现了死循环,那么就说明对flag的获取出现了问题。

t1中的这个循环是空体,这个循环在执行时的速度极快,1秒钟可以执行上百万次,而执行了这么多次load的获取结果都是一样的。另一方面,load的执行速度相比于cmp慢了太多了。此时JVM就做出来一个非常大胆的决定--不再真正的去重复load了,因为判定好像没人去修改flag的值,所以干脆就只获取一次就好了,此时就出现了前面运行的情况了。

上述的这种情况是编译器优化的一种方式,而内存可见性问题归根结底就是编译器/JVM在多线程环境下优化时产生了误判,此时就需要我们去手动干预,让编译器不要瞎搞,而这个操作结束在变量前面加上 volatile 关键字。

3.解决方法:volatile

继续挪用上面的代码,并且给flag这个变量加上volatile

class Counter{
    volatile public int flag = 0;
}
public class VolatileDemo1 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while(counter.flag == 0) {
                //循环里面不进行任何操作
            }
            System.out.println("t1 循环结束");
        });


        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入flag: ");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

此时再去运行可以看到

加了volatile之后,代码的运行情况就符合我们的预期了。

当然,代JVM并不是任何时候都会出现优化误判的情况,比如下面的代码

class Counter{
    public int flag = 0;
}
public class VolatileDemo1 {
    public static void main(String[] args) {
        Counter counter = new Counter();


        //编译器不是任何时候都会进行优化或者优化出错 如下,即使没有 volatile 也可以正常运行
        Thread t1 = new Thread(() -> {
            while(counter.flag == 0) {
                //循环里面不进行任何操作
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 循环结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入flag: ");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

我们在循环体中加入了sleep,此时代码中没有加 volatile 但是代码也可以正常运行,但是这开发中,对于这种不确定的情况,还是加上volatile更加稳妥。

4.volatile使用的注意事项

volatile 只可以对变量进行修饰,不可以对方法进行修饰。

volatile 不可以对方法中的局部变量进行修饰。

volatile 不保证原子性,若想保证原子性要使用 synchronized 

5.内存可见性问题的延伸

关于内存可见性问题,还可以从JMM(Java Memory Modle java内存模型)的角度去重新表述

Java程序里除了主内存,每个线程还有自己的“工作内存”

t1线程进行读取的时候只是读取了它工作内存的数据

t2线程进行修改的时候,先修改工作内存的数据,然后再把工作内存的数据同步到主内存中,但是由于编译器优化,导致t1没有重新从主内存中同步数据到它的工作内存中,所以读到的结果就是错误的结果。(主内存和工作内存这样的表述来自于Java文档)

上面的主内存既可以理解为前面说的内存;

而工作内存可以理解为工作存储区,也就是CPU上存储数据的单元(寄存器)以及缓存。

缓存(cache)

CPU中的寄存器存储的空间小,读写速度快,成本高;

内存的存储空间大,读写速度慢,成本低(相对于寄存器来说)

缓存就是他俩的中间值,缓存存储空间居中,读写速度居中,成本居中

当cpu在读取一个数据的时候,可能是直接读取内存,也可能是读取缓存,还可能是读取寄存器

前面说的工作内存,之所以将寄存器和缓存都包含进去,一方面是因为描述简单,另一方面,无论是缓存还是寄存器都不会对我们得到的结论产生影响。

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

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

相关文章

docker部署redis集群 删除节点(缩容)

上篇博文完成了redis集群的搭建:点这里 以及redis集群的添加节点 即扩容:点这里 本篇博文写一下怎样在redis集群中删除节点(还是在之前博文的基础上),博文中的111.111.111.111均换成实际IP使用 删除从节点 我这里想…

大数据学习:进程管理

文章目录一、进程ID(PID)二、查看进程1、进程查看命令-ps(1)命令作用(2)参数说明(3)操作案例2、Linux进程状态3、观察进程变化命令 -top(1)参数选项&#xff…

预训练模型之ELMO -《Deep contextualized word representations》论文笔记 + 高频面试题

😄 无聊学学罢了,非常简单的一个模型吧,算是一个比较经典的模型。ELMO更多的像是一个承上启下的角色,对于我们去了解那些词向量模型的思想也是很有帮助的。但由于同期的BERT等模型过于耀眼,使得大家并不太了解ELMO。 &…

微服务Sentinel流控难题:QPS模式与线程数模式区别

问题引入 不少新学Sentinel的小伙伴在配置Sentinel流控规则时犯迷糊,如下图: 其中迷糊点是阈值类型这里: QPS:当调用该接口的QPS达到阈值的时候,进行限流 线程数:当调用该接口的线程数达到阈值的时候&am…

Java 面试题 (二) -------- Java 集合相关

1、Java Bean 的命名规范 JavaBean 类必须是一个公共类,并将其访问属性设置为 public JavaBean 类必须有一个空的构造函数:类中必须有一个不带参数的公用构造器,此构造器也应该通过调用各个特性的设置方法来设置特性的缺省值。 一个 JavaB…

【云原生·k8s】k8s集群安装部署

带着理论,再去部署,验证你的理论 文章目录1、环境准备2、环境初始化3、防火墙初始化3、关闭swap4、yum源配置5、ntp配置6、修改linux内核参数,开启数据包转发功能7、安装docker基础环境()8、安装k8s的初始化工具kubead…

互联网舆情监控分析

近年来,互联网的快速发展,不论是新闻中、报纸上,还是电视里,都能屡屡看到一些企业被负面缠身,进而损害企业效益,在人人都是自媒体的时代,并非只有重大事件才会引发舆情,小事情也会&a…

kubernetes介绍和安装(1.25版本)

kubernetes介绍和安装(1.25版本) K8S 是什么? K8S官网文档:https://kubernetes.io/zh/docs/home/ K8S 是Kubernetes的全称,源于希腊语,意为“舵手”或“飞行员”,基于go语言开发,官…

liteos启动流程

一,启动流程 从这里开始我们开始讲解liteos的启动过程,通过前面连接器脚本的分析,我们已经对程序启动阶段期望的内存布局有了一个宏观的认识,然后系统上电从0x08000000地址boot起来之后要做的就是生成这个布局,然后初始化时钟,内存,任务,锁信号量等等基础的系统管理单…

leetcode 332. 重新安排行程

题目描述: 给你一份航线列表 tickets ,其中 tickets[i] [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。 所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 …

Torch.nn模块学习-池化

池化对数据起到了浓缩的效果,通过池化可以减少数据量,降低内存压力,简单地理解,池化操作都是通过池化的kernel的选取一定的区域,通过某种计算将这个区域一系列数值转化为一个数值,需要注意的是:…

【LeetCode】No.108. Convert Sorted Array to Binary Search Tree -- Java Version

题目链接:https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/description/ 1. 题目介绍(Convert Sorted Array to Binary Search Tree) Given an integer array nums where the elements are sorted in ascending …

全网最新注册ChatGPT账号攻略

OpenAI 推出超神 ChatGPT,但是由于不可抗力原因,加上网站限制,导致大部分人无法体验到。这里我分享一下注册的攻略。 前提准备 首先能能访问 Google(前置条件,不能明确说,懂得都懂)。 其次你…

利用pymupdf编辑修改pdf

利用pymupdf编辑修改pdf 本文背景 为了修改pdf的文本, 在pymupdf官方手册查了一通,没看到明显的说明,然后到github的讨论区看了发现了修改pdf的方案,在此记录一下 参考链接: https://github.com/pymupdf/PyMuPDF/discussions/1019 主要方法: 找到需要替换的文本块,然后添…

抗疫逆行者HTML网页作业 感动人物网页代码成品 最美逆行者网页模板 致敬疫情感动人物网页设计制作

🎉精彩专栏推荐 💭文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 💂 作者主页: 【主页——🚀获取更多优质源码】 🎓 web前端期末大作业: 【📚毕设项目精品实战案例 (10…

8.论文学习Liver Tumor Segmentation and Classification: A Systematic Review

目录摘要1.引言2.文献调查3.肝脏肿瘤分割的一般步骤A.CT肝脏图像B.图像预处理C.肝脏分割和肿瘤分割D.特征提取E.分类4.肝脏图像预处理方法A.中值滤波B.双边滤波器(BF)C. Wiener滤波器D.导向滤波guided filterE.递归高斯滤波Recursive Gaussian filteringF.Kirsch算子5.肝脏和肿…

基于Springboot的宠物医院管理系统-JAVA【数据库设计、论文、源码、开题报告】

1 绪论 1.1 课题背景 在信息技术高速发展的今天,新知识、新技术层出不穷,计算机技术早已广泛的应用于各行各业之中,利用计算机的强大数据处理能力和辅助决策能力叫,实现行业管理的规范化、标准化、效率化。 管理信息系统(Manag…

HummerRisk V0.6.0发布:升级列表高级搜索功能,扩充对象存储和操作审计支持范围等

HummerRisk V0.6.0发布:新增表头高级搜索功能,可按名称快速搜索与组合查询,动态调整显示列,新增对象存储七牛云与青云类型,新增操作审计火山引擎(火山云)类型。 感谢社区中小伙伴们的反馈&…

用DIV+CSS技术设计的西安旅游网站18页(web前端网页制作课作业)HTML+CSS旅游网站设计与实现

👨‍🎓静态网站的编写主要是用 HTML DⅣV CSSJS等来完成页面的排版设计👩‍🎓,一般的网页作业需要融入以下知识点:div布局、浮动定位、高级css、表格、表单及验证、js轮播图、音频视频Fash的应用、uli、下拉…

C# WPF 基础等待动画Loading...动态转圈 Storyboard ContentControl

这个效果图...直接放上吧&#xff0c;实际是转圈效果&#xff0c;使用起来最方便的一种。 【这是个基础版&#xff0c;灵活度很高】 Xaml 绘制Loading图案&#xff0c;及触发的动画效果&#xff0c;实际控制的每个组件 Opacity - 透明度 属性。 <Style TargetType"{x…