Java 内存模型深度解析

优质博文:IT-BLOG-CN

一、并发编程模型的两个关键问题

【1】并发中常见的两个问题:线程之间如何通信及线程之间如何同步。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:内存共享和消息传递
【2】在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共数据进行隐式通信。在消息传递的并发模型里,如果没有公共状态,线程之间必须通过发送消息来显示进行通讯;
【3】同步是指程序中用于控制不同线程间操作发生相对顺序的机制,可以理解为协同步调,按预定的先后次序运行。这里的“同”字应是指协同、协助、互相配合的意思而非一起的意思。可理解为线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行,B执行结束后再将结果给A,A再继续操作。在共享内存并发模型里,同步是显式进行的。在消息传递的并发模型里,由于消息的发送必须在消息接收之前。因此同步是隐式进行的;
【4】Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的 Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题;

二、Java内存模型的抽象结构

【1】Java中堆内存主要用于存储实例域、静态域和数组元素等,堆内存在线程之间共享(“共享变量”指实例域、静态域和数组元素)。局部变量(Local Variables),方法定义参数和异常处理器参数(Exception Handler Parameters)不会在线程之间共享,它们不会有内存可见性问题,也不会受内存模型的影响。
【2】Java 线程之间的通讯由 Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是 JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓存区、寄存器以及其它的硬件和编译器优化。Java内存模型的抽象示意图如下:

从上图来看,如果线程A和线程B之间要通信的话,必须要经历下面2个步骤:
 1)、线程A把本地内存A中更新过的共享变量刷新到主内存中去。
 2)、线程B到内存中去读取线程A之前已更新过的共享变量。
从整体上来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。

三、从源代码到指令序列的重排序

在执行程序时,为了提高性能,编译器(javac:将 java源程序编译成中间代码字节码文件)和处理器常常会对指令做重排序。重排序分为三种:
 1)、编译器优化的重排序。编译器再不改变单线程程序语义的前提下,可以重新安排语句的性质顺序。
 2)、指令集并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
 3)、内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序中执行。从 Java源代码到最终实际执行的指令序列,会分别经历下面重排序:

【1】上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器排序,JMM的处理器重排序规则会要求 Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Inter称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
【2】JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

四、并发编程模型的分类

现在的处理器使用写缓冲区临时保存向内存写入的数据,因为处理器的处理速度远远大于 IO的处理速度。写缓冲区能够保证指令流水线运行,可以有效的避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓存器中内存地址相同的多次写操作,减少对内存总线的占用。虽然写缓存区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见,这个特性会对内存操作的执行产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致。举个栗子:

示例列/处理器ProcessAProcessB
代码a=1;//A1x=b;//A2b=2;//B1y=a;//B2
运行结果初始状态a=b=0
处理器运行执行后得到的结果:x=y=0

假设处理器A 和处理器B 按程序的顺序并行执行内存访问,最终可能得到 x=y=0的结果。具体原因:就是上面所说的,处理器上的缓存区仅可见于它所在的处理器。

【1】这里处理器A 和处理器B 可以同时把共享变量写入自己的缓冲区(A1、B1),然后从共享内存中读取另一个共享变量(A2、B2),最后才把自己写缓存中保存的脏数据刷新到内存中(A3、B3),当以这种时序执行时,程序就可以得到 x=y=0的结果。
【2】从内存操作实际发生的顺序来看,直到处理器A执行A3来刷新自己的写缓存区,写操作A1才算真正执行了。虽然处理器A执行内存操作的顺序为:A1——>A2,但实际内存操作的顺序A2——>A1。此时处理器A 的内存操作顺序被重排序了(处理器B相同)
【3】这里关键是,由于写缓存区仅对自己的处理器可见,从而导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现在的处理器都支持写缓存,因此现在的处理器都支持写-读操作的重排序。
▶ 常见处理器允许的重排序类型的列表:(N:表示不允许重排序)

处理器/规则Load-LoadLoad-StoreStore-StoreStore(写)-Load(读)数据依赖
SPARC-TSONNNYN
X86NNNYN
IA64YYYYN
PowerPCYYYYN

可以看出常见的处理器都允许 Store-Load重排序,常见的处理器都不允许存在数据依赖的操作重排序。Sparc-TSO 和 X86拥有相对较强的处理器内存模型,它们仅允许对写-读操作做重排序(因为它们都使用了写缓冲区)。为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障指令分为4类:

屏障类型指令示例说明
LoadLoad BarriersLoad1; LoadLoad; Load2确保Load1装载数据优先Load2及所有后续装载指令的装载。
StoreStore BarriersStore1; StoreStore; Store2确保Store1数据对其他处理器可见(刷新到内存)优先Store2及所有后续存储指令的存储。
LoadStore BarriersLoad1; LoadSotre; Store2确保Load1数据装载优先于Store2及所有后续的存储指令刷新到内存。
StoreLoad BarriersStore1; StoreLoad; Load2确保Store1中的数据对其他处理器可见(刷新到内存)优于Load2及所有的后续加载指令。StoreLoad Barriers会使该屏障是前的所有内存访问指令(存储和装载指令)完成后,才执行该屏障后的内存访问指令

StoreLoad Barriers是一个“全能型”的屏障,它同时具有前面3个屏障的效果。现在的大多数处理器都支持该屏障(其他类型的屏障不一定被支持),支持开屏障开销一般很昂贵,因为当前处理器通常要把写缓存中的数据全部刷新到内存中(Buffer Fully Fulsh)。

五、happens-before 简介

JSR-133内存模型使用 happens-before的概念来阐述操作之间的内存可见性。JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before关系。这两个操作既可以是同一个线程内,也可以是在不同线程之间。
程序员密切相关的 happens-before规则如下:
【1】程序顺序规则: 一个线程中的每个操作,happens-before于该线程中的任意后续操作。
【2】监视器锁规则: 对一个锁的解锁,happens-before于随后对这个锁的加锁。
【3】volatile变量规则: 对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
【4】传递性: 如果A happens-before B,且B happens-before C,那么A happens-before C。

【注意】: 两个操作具有 happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。happens-before定义很微妙,后文具体说明 happens-before为什么要这么定义。

一个 happens-before规则对应于一个或多个编译器和处理器重排序规则。对于Java程序员来说,happens-before规则简单易懂,它避免了程序员为了理解 JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Redis 存在线程安全问题吗?为什么?

一个工作了 5 年的粉丝私信我。 他说自己准备了半年时间,想如蚂蚁金服,结果第一面就挂了,非常难过。 问题是: “Redis 存在线程安全问题吗?” 一、问题解析 关于这个问题,我从两个方面来回答。 第一个&a…

ChatGPT 到 Word:使用 Writage 进行复制粘贴魔法

ChatGPT 到 Word:使用 Writage 进行复制粘贴魔法 写在前面Writage的使用 写在前面 随着ChatGPT的日益普及,越来越多的人每天依赖它来完成各种任务。无论是寻找信息、语言翻译、解决数学问题,还是精炼复杂的概念和文本,ChatGPT 都…

AWS CI/CD之二:配置CodeDeploy

问题 前面一篇文章介绍了CodeBuild中构建一个Java的Maven项目。在这个基础上面,我们继续AWS CI/CD工作流构建之路。 1.配置CodePipeline简配版 这里主要是利用CodePipeline配置之前的CodeBuild项目,以便生产出需要部署的jar文件和CodeDeploy需要用到相…

【rust/bevy】使用points构造ConvexMesh

目录 说在前面问题提出Rapier具体实现参考 说在前面 操作系统:win11rust版本:rustc 1.77.0-nightlybevy版本:0.12 问题提出 在three.js中,可以通过使用ConvexGeometry从给定的三维点集合生成凸包(Convex Hull) import { ConvexGeo…

【c++】——栈or队列or优先级队列

目录 🎓容器适配器 🎓Stack栈 🚩Stack的介绍 🚩Stack的基本使用 🚩Stack底层实现 🎓queue队列 🚩queue的介绍 🚩queue的基本使用 🚩queue的底层实现 &#x1…

爬虫之牛刀小试(八):爬取微博评论

今天爬取的是微博评论。 可以发现其特点是下一页评论的max_id在上一页中。 于是代码如下: import requests import json import re import time headers {User-Agent: ,"Cookie": "","Referer": "https://m.weibo.cn/detail/4…

Kafka-消费者-KafkaConsumer分析-PartitionAssignor

Leader消费者在收到JoinGroupResponse后,会按照其中指定的分区分配策略进行分区分配,每个分区分配策略就是一个PartitionAssignor接口的实现。图是PartitionAssignor的继承结构及其中的组件。 PartitionAssignor接口中定义了Assignment和Subscription两个…

网络安全全栈培训笔记(54-服务攻防-数据库安全RedisHadoopMysqla未授权访问RCE)

第54天 服务攻防-数据库安全&Redis&Hadoop&Mysqla&未授权访问&RCE 知识点: 1、服务攻防数据库类型安全 2、Redis&Hadoop&Mysql安全 3、Mysql-CVE-2012-2122漏洞 4、Hadoop-配置不当未授权三重奏&RCE漏洞 3、Redis-配置不当未授权…

Laya3.0 相机使用

摄像机,是3D场景里边最经常使用的对象了。 官方文档:点击这里学习 1.投影 Projection 透视: 模拟人眼的视觉效果,近大远小。模拟物理世界的规律,将眼睛或相机抽象成一个点,此时视锥体内的物体投影到视平…

51单片机独立按键

独立按键介绍 在嵌入式系统中,独立按键通常指的是单独的按键开关或按钮,它们通常用于接收用户输入或执行特定的功能。在51单片机(指的是Intel 8051或其兼容芯片)中,独立按键可以通过简单的硬件连接和软件编程来实现各种…

Grafana(三)Grafana 免密登录-隐藏导航栏-主题变换

一. 免密登录 Grafana 的常用方式: 将配置好的Grafana图嵌入到系统页面中 为了实现可免登录访问,可以通过如下方式进行设置: 1. 修改Grafana配置文件 在Grafana的配置文件 /etc/grafana/grafana.ini 中,找到 [auth.anonymous] 配…

网络编辑day4

思维导图 广播模型发送端-->类似于UDP客户端 #include<head.h> int main(int argc, const char *argv[]) {//1、创建套接字int sfdsocket(AF_INET,SOCK_DGRAM,0);if(sfd-1){perror("socket error ");return -1;}//2、将套接字设置成允许广播int broadcast1…

【SpringCloud】微服务框架后端部署详细过程记录20240119

前言&#xff1a;前两天公司接到客户提供的一个微服务框架&#xff0c;导师让我在本地部署验证一下该框架的可用性&#xff0c;借此机会记录一下微服务项目的一个基本部署流程&#xff0c;仅供学习参考&#xff0c;如有不足还请指正&#xff01; 文件结构 提供的压缩文件共包含…

【lettuce-排行榜】

背景&#xff1a; 这次游戏中台采用lettuce的zset完成游戏内的本服和跨服排行榜&#xff0c;因此写一下案例。 pom.xml <dependency><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId><version>6.2.4.RELEASE</ve…

Android14之DefaultKeyedVector实现(一百八十二)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

python之粘包/粘包的解决方案

python之粘包/粘包的解决方案 什么是粘包 粘包就是在数据传输过程中有多个数据包被粘连在一起被发送或接受 服务端&#xff1a; import socket import struct# 创建Socket Socket socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 绑定服务器和端口号 servers_addr (…

LeetCode 热题 100 | 双指针(上)

目录 1 283. 移动零 2 11. 盛最多水的容器 3 15. 三数之和 菜鸟做题第一周&#xff0c;语言是 C 1 283. 移动零 解题思路&#xff1a; 两个指针一前一后遍历数组前者永远指向 0&#xff0c;后者永远在寻找非 0 数的路上后者找到一个非 0 数就和前者进行一个数值交换 …

Python爬虫从入门到入狱系列合集

我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448; 入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448; 虚 拟 环 境 搭 建 &#xff1a;&#x1f449;&…

linux下USB抓包和分析流程

linux下USB抓包和分析流程 在windows下抓取usb包时可以通过wireshark安装时安装USBpcap来实现usb抓包&#xff0c;linux下如何操作呢&#xff1f; 是基于usbmon&#xff0c;本博客简单描述基于usbmon在linux系统上对通过usb口进行发送和接收的数据的抓包流程&#xff0c;分别描…

Unity SnapScrollRect 滚动 匹配 列表 整页

展示效果 原理: 当停止滑动时 判断Contet的horizontalNormalizedPosition 与子Item的缓存值 相减,并得到最小值&#xff0c;然后将Content horizontalNormalizedPosition滚动过去 使用方式&#xff1a; 直接将脚本挂到ScrollRect上 注意&#xff1a;在创建Content子物体时…