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提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python利用Excel读取和存储测试数据完成接口自动化教程

http_request2.py用于发起http请求 #读取多条测试用例#1、导入requests模块import requests#从 class_12_19.do_excel1导入read_data函数from do_excel2 import read_datafrom do_excel2 import write_datafrom do_excel2 import count_case#定义http请求函数COOKIENonedef ht…

深入理解Redis数据结构

目录 Redis的单线程 Redis单线程快的原因 Redis 单线程处理高并发客户端连接 Redis数据结构 字符串(String) 常用方法 数据结构 哈希表(Hash) 常用方法 数据结构 列表(List) 常用方法 数据结构…

微信小程序之WXML 模板语法之数据绑定、事件绑定、wx:if和列表渲染

学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。各位小伙伴,如果您: 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持,想组团高效学习… 想写博客但无从下手,急需…

Java深度解析:类的生命周期与类加载过程

文章目录 加载阶段连接阶段-验证连接阶段-准备连接阶段-解析初始化阶段使用卸载 最近在学习Java 虚拟机,学到了类的声明周期,有些比较难理解的点,特地来总结一下。 类的生命周期从大体上来看的话,有五个阶段,分别是加载…

十八周周报

文章目录 摘要文献阅读3D reconstruction of human bodies from single-view and multi-view images: A systematic review简介研究方法搜索策略选择标准搜索结果 三维重建方法单个视图中使用的技术基于参数化人体模型的回归基于非参数人体模型的回归 多个视图中使用的技术基于…

ARMv8-AArch64 的异常处理模型详解之异常类型 Exception types

异常类型详解 Exception types 一, 什么是异常二,同步异常(synchronous exceptions)2.1 无效的指令和陷阱异常(Invalid instructions and trap exceptions)2.2 内存访问产生的异常2.3 产生异常的指令2.4 调…

使用残差网络识别手写数字及MNIST 数据集介绍

MNIST 数据集已经是一个几乎每个初学者都会接触的数据集, 很多实验、很多模型都会以MNIST 数据集作为训练对象, 不过有些人可能对它还不是很了解, 那么今天我们一起来学习一下MNIST 数据集。 1.MNIST 介绍 MNIST 数据集来自美国国家标准与技术研究所, National Institute of S…

Docker 47 个常见故障的原因和解决方法

【作者】曹如熙,具有超过十年的互联网运维及五年以上团队管理经验,多年容器云的运维,尤其在Docker和kubernetes领域非常精通。 Docker是一种相对使用较简单的容器,我们可以通过以下几种方式获取信息: 1、通过docker r…

HNU-编译原理-实验1-利用FLEX构造C-Minus-f词法分析器

编译原理实验1利用FLEX构造C-Minus-f词法分析器 计科210X 甘晴void 202108010XXX 实验要求 详细的实验项目文档为 https://gitee.com/coderwym/cminus_compiler-2023-fall/tree/master/Documentations/lab1 学习和掌握词法分析程序的逻辑原理与构造方法。通过 FLEX 进行实…

[陇剑杯 2021]webshell

[陇剑杯 2021]webshell 题目做法及思路解析(个人分享) 问一:单位网站被黑客挂马,请您从流量中分析出webshell,进行回答: 黑客登录系统使用的密码是_____________。 题目思路: 分析题目&…

【蓝桥备赛】求阶乘

题目链接 求阶乘 个人想法 之前做过计算阶乘结果后面有几个0的题目,这里看到本题之后,很快就有思路了。想要得到阶乘结果有几个0,首先尾数后面的0,最小肯定是因为因子中存在10。然后,10如何得来呢? 2 * …

【面试突击】硬件级别可见性问题面试实战(中)

🌈🌈🌈🌈🌈🌈🌈🌈 欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送! 在我后台回复…