目录
volitale浅析

内存模型的相关概念

计算机中执行程序时,每条指令都是在CPU中执行,执行指令的过程必然会涉及到数据的读取和写入。而程序运行时的数据是存放在主存(物理内存)中,由于CPU的读写速度远远高于内存的速度,如果CPU直接和内存交互,会大大降低指令的执行速度,所以CPU里面就引入了高速缓存。

脑补当初学习OS时的图 CPU->内存 CPU->寄存器->内存

​ 也就是说程序运行时,会将运算所需要的数据从主存中复制一份到高速缓存,CPU进行计算的时候可以直接从高速缓存读取和写入,当运算结束时,在将高速缓存中的数据刷新到主存。

​ 但是如果那样必须要考虑,在多核CPU下数据的一致性问题怎么保证?比如i=i+1,当线程执行这条时,会先从主存中读取i的值,然后复制一份到高速缓存,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。在单线程下这段代码运行不会存在问题,但如果在多线程下多核CPU中,每个CPU都有自己的高速缓存,可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

为了解决缓存不一致性问题,通常来说有以下2种解决方法:

  1. 通过在总线加LOCK#锁的方式

  2. 通过缓存一致性协议

    图片描述

原子性可见性有序性

并发编程中,通常会考虑的三个问题原子性问题、可见性问题、有序性问题。

(1)原子性:程序中的单步操作或多步操作要么全部执行并且执行的过程中不能被打断,要么都不执行。

如果程序中不具备原子性会出现哪些问题?

转账操作就是一个很好的代表,如果转账的过程中被中断,钱转出去了,由于中断,收账方却没有收到。

(2)可见性:内存可见性,指的是线程之间的可见性,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。

java
复制代码
//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;

倘若线程1从主存中读取了i的值并复制到CPU高速缓存,然后对i修改为10,这时CPU高速缓存中的i值为10,在没有将高速缓存中的值刷新到主存中时,线程2读取到的值还是0,它看不到i值的变化,这就是可见性问题。

Java提供了Volatile关键字来保证可见性,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

  而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

  另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

(3)有序性:程序执行的顺序按照代码的先后顺序执行。

实际是这样吗?

java
复制代码
int i = 0;	//[1]
int a,b;	//[2]

[2]一定会在[1]之后执行吗?不一定,在JVM中,有可能会发生指令重排序(Instruction Reorder)。如果[1]、[2]中有相互依赖,比如[2]中的数据依赖于[1]的结果,那么则不会发生指令重排序。

什么是指令重排序?

一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

​ 指令重排对于提⾼CPU处理性能⼗分必要。虽然由此带来了乱序的问题,但是这点牺牲是值得的。

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

Java内存模型的抽象结构

JVM可以看做是一个有OS架构的处理机,他也有自己的内存和处理器,它的内存和之前讨论的没有什么太大的差异。

Java运行时内存的划分如下:

图片描述

对于每⼀个线程来说,栈都是私有的,而堆是共有的。也就是说在栈中的变量(局部变量、⽅法定义参数、异常处理器参数)不会在线程之间共享,也就不会有内存可⻅性(下⽂会说到)的问题,也不受内存模型的影
响。⽽在
堆中的变量是共享的,本⽂称为共享变量
。所以内存可见性针对的是共享变量。

1、既然堆是共享的,为什么在堆中会有内存不可⻅问题?

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

线程之间的共享变量存在主内存中,每个线程都有⼀个私有的本地内存,存储了该线程以读、写共享变量的副本。本地内存是Java内存模型的⼀个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器等。

Java线程之间的通信由Java内存模型(简称JMM)控制,从抽象的⻆度来说,JMM定义了线程和主内存之间的抽象关系。JMM的抽象示意图如图所示:

图片描述

从图中可以看出:

  1. 所有共享变量都存在主内存中。
  2. 每个线程都保存了一份该线程使用到的共享变量的副本。
  3. 线程A与线程B之间的通信必须通过主存。

2、JMM与Java内存区域划分的区别与联系

  • 区别

    JMM是抽象的,他是⽤来描述⼀组规则,通过这个规则来控制各个变量的访问⽅式,围绕原⼦性、有序性、可⻅性等展开的。⽽Java运⾏时内存的划分是具体的,是JVM运⾏Java程序时,必要的内存划分。

  • 联系

    都存在私有数据区域和共享数据区域。⼀般来说,JMM中的主内存属于共享数据区域,他是包含了堆和⽅法区;同样,JMM中的本地内存属于私有数据区域,包含了程序计数器、本地⽅法栈、虚拟机栈。

原子性、可见性、有序性

Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

volatile的内存语义

在Java中,volatile关键字有特殊的内存语义。volatile主要有以下两个功能:

  • 保证变量的内存可⻅性
  • 禁⽌volatile变量与普通变量重排序(JSR133提出,Java 5 开始才有这个“增强的volatile内存语义“)

内存可见性

所谓内存可见性,指的是当一个线程对volatile修饰的变量进行过写操作时,JMM会立即把线程对应的本地内存中的共享变量的值刷新到主内存;当一个线程对volatile修饰的变量进行读操作时,JMM会立即把该线程对应的本地内存置为无效,从内存中从新读取共享变量的值。

禁止重排序

JMM是通过==内存屏障==来限制处理器对指令的重排序的。

什么是内存屏障?硬件层面,内存屏障分两种:读屏障(Load Barrier)和写屏障(Store Barrier)。内存屏障有两个作用:

  1. 阻止屏障两侧的指令重排序
  2. 强制把写缓冲区/⾼速缓存中的脏数据等写回主内存,或者让缓存中相应的数据 失效。

通俗说,通过内存屏障,可以防止指令重排序时,不会将屏障后面的指令排到之前,也不会将屏障之前的指令排到之后。

Volatile关键字的应用场景

单例模式下的Double-Check(双重锁检查)

java
复制代码
public class Singleton {
    public static Singleton instance;
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { 			//[1]
            synchronized (Singleton.class) {
                instance = new Singleton(); //[2]
            }
        }
        return instance;
    }
}

如果这里的变量没有使用volatile关键字,那么有可能就会发生错误。

[2]实例化对象的过程可以分为分配内存、初始化对象、引用赋值。

java
复制代码
instance = new Singleton(); // [1]
// 可以分解为以下三个步骤
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址
// 上述三个步骤可能会被重排序为 1-3-2,也就是:
1 memory=allocate();// 分配内存 相当于c的malloc
3 s=memory //设置s指向刚分配的地址
2 ctorInstanc(memory) //初始化对象

如果一旦发生了上述的重排序,当程序执行了1和3,这时线程A执行了if判断,判定instance不为空,然后直接返回了一个未初始化的instance。

参考:Java并发编程:volatile关键字解析

写在最后,从疫情到现在,这是发的第一篇帖子,也算是对白嫖康师傅的视频买了一个小单吧。2019寒假-遭遇疫情-2020暑假,一年又过去了一半,感觉时间时快时慢,认真思考问题的时候时间是飞快的,然而当停止脚步的时候,时间又好像静止了。大三的整个下学期都是在家里度过的,其中有一段时间是自己一个人在家经历了漫长的5、6个月,期间主要以回顾基础为主,比如说数据结构与算法、Java,然后入坑了一些热门框架等等,当时给自己的要求是每天都要学一点数据结构,虽然不能每天坚持,但总体完成度来说达到了90%吧(个人)。自己从新整理了一遍Java,重点突击了自己的懵懂点未知点,可以说是受益匪浅,读书百遍,其义自见。
学习的过程是一个孤独和难熬的过程,自己一个人在家学习是这样觉得的,没有像学校那样,有同学在一起学习的氛围,就好比培训班差不多吧,报培训的好处是,当你在其中时,你会被其他人努力奋进的样子所触发,从而变相激励自己,努力学习,证明自己。所以说,受得了孤独,也是很厉害的。

接下来主要是准备秋招了,只怪自己当初了解的太少,做的打算太少,计划太少,当了解到秋招之后的几个星期,每天都是抑郁度过,没什么动力,但never giving up,现在基本上开始准备秋招的事情了,重振旗鼓。

1个月准备秋招来得及吗?希望各位帅哥美女给点建议,小弟感激涕零!

行我所行,尽我所能!心里有梦,眼里有光!

本文由 康师傅的小跟班 原创发布于 阳光沙滩 , 未经作者授权,禁止转载
评论
0 / 1024
用户头像
你这里说的内存模型是对的,好多人都错把内存模型当成了JVM的内存结构
2020-09-21 08:59回复
推荐文章
一篇文章讲清楚雅思、托福、GRE、SAT、GMAT:它们是什么?有什么区别?应该考哪个?
面对IELTS、TOEFL、GRE、SAT和GMAT,你是否感到困惑?本文深度解析五大考试的核心区别:从语言证明到学术能力评估,明确各自适用人群与场景。特别更新2026年托福改革及最新评分体系,通过对比表格与真实申请案例,助你精准定位目标考试,避免无效备考,高效规划海外升学之路。
一篇文章讲清楚 Android 签名:PK8、X509.PEM、JKS 是什么?它们有什么关系?
深入解析Android应用与系统签名的核心文件:PK8、X509.PEM与JKS。本文从非对称加密原理出发,厘清私钥、证书与密钥库的本质区别及相互转换方法,涵盖AOSP平台签名与普通应用发布的实际场景。通过对比V1-V4签名方案及解答常见疑难,助你掌握保障APK完整性与身份认证的关键技术,规避签名丢失或冲突风险。
系统监控运维系统
你是否也深受证书过期、磁盘满载等运维痛点的折磨?排查耗时,影响线上体验。本文分享一款自研运维系统面板,集成证书统一监测、资源预警及WebSSH等功能,实现问题快速定位与处理。附代码共享链接,欢迎参考或二次开发,提升运维效率。
断点说搞个万年历,怎么搞?
想快速拥有一个专属的程序员日历?只需一篇清晰的设计文档,剩下的交给AI!本文带你从零搭建“sob-calendar”项目,集成农历、节气及独特的程序员运势功能。通过实际案例展示如何利用AI高效生成代码并部署上线,让繁琐的开发工作变得轻松有趣,快来体验科技与传统历法的奇妙碰撞吧!
linux服务器的日志、容器清理
本手册提供了一套全面的Linux服务器磁盘清理与排查方法,帮助用户高效管理磁盘空间。通过查看磁盘使用情况、分析Docker占用、清理无用容器和日志,确保系统运行流畅。适合需要优化服务器性能的运维人员参考。
记给网站接入图形验证码
本文分享了如何为个人站点添加图形验证码以提升安全性,详细介绍了使用第三方接口实现验证码功能的过程,并通过代码示例展示了具体实现方式。对于关注网站安全的开发者来说,这篇文章提供了实用的参考价值。
Java开发环境搭建
本文是一份详细的Java开发环境搭建指南,涵盖了Java的发展历史、版本选择及安装配置过程。无论你是初学者还是有经验的开发者,都能从中找到实用的信息和步骤,帮助你快速上手Java开发。
Vue3.js项目的创建
本文详细介绍了如何使用Vue3创建项目,包括Node.js的安装和配置,以及项目的初始化过程。适合初学者学习如何快速搭建Vue3开发环境,并推荐了相关课程供进一步学习。
Network Tools : 您的运维“助手”升级了
Network Tools是一款专为解决运维难题而设计的软件,集设备管理、配置备份、性能监控和告警处理于一体。通过统一的工作台,帮助运维人员更高效地管理资产、定位故障并提升团队协作效率。无论是设备在线状态还是具体问题排查,都能得到全面支持。
Ubuntu 软件安装与彻底卸载指南
本文详细介绍了在 Ubuntu 系统中如何规范安装和彻底卸载 .deb 格式软件,包括依赖修复、残留清理以及图形化工具推荐,适合需要维护系统环境的用户。
JavaWeb开发客户程序更新特殊环境下的骚操作
本文分享了在无外网环境下,如何利用Docker和docker-compose实现高效部署的实战经验。通过基础镜像和本地jar包挂载的方式,解决了内网环境下的依赖问题,适合需要灵活部署方案的技术人员参考。
Linux 防火墙完全指南:从原理到多发行版实战
本文全面解析Linux防火墙的底层原理与主流工具配置,涵盖UFW、firewalld、iptables、nftables等方案,适合不同发行版用户。从基础概念到生产环境加固建议,助你掌握核心安全策略,确保服务器与网络的安全运行。
水上派对英语单词篇
探索FLOATOPIA水上派对的完整指南,涵盖签到流程、场地指引、安全须知、活动安排及夜场舞会等实用英语词汇与对话。无论是初学者还是老手,都能轻松掌握关键术语,享受完美水上体验。
麒麟操作系统下使用 virt-customize 重置虚拟机 root 密码实战
本文详细介绍了在麒麟操作系统环境下使用 virt-customize 工具重置 KVM 虚拟机 root 密码的完整流程,包含安装步骤、常见问题及解决方案,帮助运维人员高效处理密码遗忘或批量初始化场景,避免数据丢失。
浅析JS预编译-函数篇
本文详细讲解了JavaScript中函数的预编译过程,包括AO对象的创建、变量和函数声明的提升,以及执行阶段的变化。适合初学者了解JS执行机制,帮助深入理解代码运行逻辑。
访问Github的另一种姿势
本文介绍了如何通过Watt ToolKit等工具加速访问Github,适合对网络技术感兴趣的读者。内容涵盖工具介绍、下载和使用方法,以及一些实用技巧,值得一看。
使用Gulp压缩JS
本文详细介绍了如何使用Gulp压缩JavaScript文件,特别是支持ES6语法的处理方法。通过实际操作和效果对比,帮助开发者提升代码性能,适合对前端自动化工具感兴趣的读者。
使用Gulp压缩CSS
本文详细介绍了如何使用Gulp工具对CSS文件进行压缩,提升网页加载速度。适合需要优化静态资源的开发者,提供清晰的步骤和示例,帮助快速上手。
从0部署Nuxtjs项目
本文详细介绍了如何将Nuxt.js项目部署到公网,涵盖Node.js安装、环境配置、源码上传及使用PM2启动服务的全过程,适合开发者学习和实践。
Linux 命令行美化利器:tree 命令完全指南(安装 + 全部参数详解)
掌握 Linux 下的 `tree` 命令,快速直观地查看目录结构。本文详细讲解安装方法、参数含义及实战场景,适合开发者和系统管理员提升工作效率。
Linux常用命令(一)
本文详细介绍了Linux基础命令,包括文件操作、系统显示、网络状态、软件包管理等,适合初学者学习和查阅,是掌握Linux系统的实用指南。
大模型推理参数解码:温度、Top-p 与核采样如何塑造生成文本的灵魂
探索大语言模型的采样参数如何塑造生成内容,从温度到Top-p,从惩罚机制到熵控制,揭示背后的信息论原理与实践策略。了解如何通过参数调优,让AI既保持知识的严谨,又拥有创造力的自由。
记从0使用Claude code写代码
本文介绍了如何使用Claude Code搭配DeepSeek进行编程,详细讲解了安装Node.js、全局安装Claude、配置模型以及使用方法。对于开发者来说,这是一份实用的技术教程,尤其适合对AI编程工具感兴趣的读者。
Android-Studio-Gradle同步失败-Library为null的通用排查指南
遇到 Android Studio Gradle Sync 失败时,不要轻易删除全局缓存。本文详细解析了错误原因,并提供了一系列精准的排查步骤和解决方案,帮助开发者快速定位并解决问题,提升开发效率。
PHP实现密码加密
本文详细介绍了Bcrypt密码加密技术及其在PHP中的应用,帮助开发者提升用户密码的安全性。通过实际代码示例,展示了如何使用password_hash和password_verify函数进行密码加密与验证,是学习网络安全知识的实用指南。
WordCloud效果,滚动标签
本文详细介绍了如何在Vue项目中集成WordCloud组件,包括依赖安装、属性配置和使用方法。适合开发者学习如何实现数据可视化功能,提升项目交互体验。
从0使用WordPress搭建一个优美的网站
本文详细介绍了如何使用WordPress搭建一个美观的网站,从安装到主题配置和功能拓展,为读者提供了实用的操作指南。无论是初学者还是有一定经验的开发者,都能从中获得有价值的参考。
JS实现在网站底部添加运行时间
想知道如何在网站底部显示运行时间?本文详细讲解了通过JavaScript实现这一功能的方法,包括时间计算逻辑和代码实现。适合前端开发者学习参考,轻松为网站添加实用功能。
在Vercel上部署Hexo博客
本文详细介绍了如何在Vercel上快速部署Hexo博客,无需后端服务即可实现高效发布。相比传统方式,省去了手动生成和上传静态文件的步骤,更加便捷。适合想要搭建个人博客的开发者参考。
从0搭建一个Hexo博客
本文详细介绍了Hexo博客框架的使用方法,从安装到部署全流程讲解,适合想快速搭建个人博客的技术爱好者。内容清晰易懂,是入门Hexo的理想指南。