05.深入理解JMM和Happens-Before

news2025/1/23 17:53:38

JMM都问啥?

最近沉迷P5R,所以写作的进度很不理想,但不得不说高卷杏YYDS。话不多说,开始今天的主题,JMM和Happens-Before

关于它们的问题并不多,基本上只有两个:

  • JMM是什么?详细描述下JMM。
  • 说说你对JMM的理解,为什么要这样设计?

Tips:本文以JMM理论为主。

JMM是什么?

JMM即Java Memory Model,Java内存模型。JSR-133 FAQ中对内存模型的解释是:

At the processor level, a memory model defines necessary and sufficient conditions for knowing that writes to memory by other processors are visible to the current processor, and writes by the current processor are visible to other processors.

处理器级别上,内存模型定义了处理器核心间对彼此写内存操作可见性的充要条件。以及:

Moreover, writes to memory can be moved earlier in a program; in this case, other threads might see a write before it actually "occurs" in the program.  All of this flexibility is by design -- by giving the compiler, runtime, or hardware the flexibility to execute operations in the optimal order, within the bounds of the memory model, we can achieve higher performance.

在内存模型允许的范围内,允许编译器、运行时或硬件以最佳顺序执行指令,以提高性能。最佳顺序是通过指令重排序得到的指令执行顺序。

我们对处理器级别的内存模型做个总结:

  • 定义了核心间的写操作的可见性
  • 约束了指令重排序

接着看对JMM的描述:

The Java Memory Model describes what behaviors are legal in multithreaded code, and how threads may interact through memory.It describes the relationship between variables in a program and the low-level details of storing and retrieving them to and from memory or registers in a real computer system.It does this in a way that can be implemented correctly using a wide variety of hardware and a wide variety of compiler optimizations.

提取这段话的关键信息:

  • JMM描述了多线程中行为的合法性,以及线程间如何通过内存进行交互
  • 屏蔽了硬件和编译器的实现差异,以达到一致的内存访问效果

我们结合内存模型来看,JMM到底是什么?

  • JVM的角度看,JMM屏蔽了不同硬件/平台底层差异,达到一致的内存访问效果
  • Java开发人员的角度看,JMM定义了线程间写操作的可见性,约束了指令重排序

那么为什么要有内存模型呢?

“诡异”的并发问题

02.关于线程你必须知道的8个问题(上)中给出了并发编程的3要素,以及无法正确实现带来的问题,接下来我们探究下底层原因。

Tips:补充一点Linux中线程调度相关内容。

Linux线程调度是基于时间片的抢占式调度,简单理解为,线程尚未执行结束,但时间片耗尽,线程挂起,Linux在等待队列中选取优先级最高的线程分配时间片,因此优先级高的线程总会被执行

上下文切换带来的原子性问题

我们以常见的自增操作count++为例。

直觉上我们认为自增操作是一气呵成,没有任何停顿。但实际上会产生3条指令:

  • 指令1:将count读入缓存;
  • 指令2:执行自增操作;
  • 指令3:将自增后的count写入内存。

那么问题来了,如果两个线程t1,t2同时对count执行自增操作,且t1执行完指令1后发生了线程切换,此时会发生什么?

我们期望的结果是2,但实际上得到1。这便是线程切换带来的原子性问题。那么禁止线程切换不就解决了原子性问题吗?

虽然是这样,但禁止线程切换的代价太大了。我们知道,CPU运算速度“贼快”,而I/O操作“贼慢”。试想一下,如果你正在用steam下载P5R,但是电脑卡住了,只能等到下载后才能愉快的写BUG,你气不气?

因此,操作系统中线程执行I/O操作时会放弃CPU时间片,让给其它线程,提高CPU的利用率

P5R天下第一!!!

缓存带来的可见性问题

你可能会想上面例子中,线程t1,t2操作的不是同一个count吗?

看起来是同一个count,但其实是内存中count在不同缓存中的副本。因为,不仅是I/O和CPU有着巨大的速度差异,内存与CPU的差异也不小,为了弥补差异而在内存和CPU间添加了CPU缓存。

CPU核心操作内存数据时,先拷贝数据到缓存中,然后各自操作缓存中的数据副本。

我们先忽略MESI带来的影响,可以得到线程对缓存中变量的修改对其它线程来说并不是立即可见的

Tips:拓展中补充MESI协议基础内容。

指令重排序带来的有序性问题

除了以上提升运行速度的方式外,还有其它“幺蛾子”--指令重排序。我们把02.关于线程你必须知道的8个问题(上)中的例子改一下。

public static class Singleton {
	private Singleton instance;
	public Singleton getInstance() {
	    if (instance == null) {
		    synchronized(this) {
			    if (instance == null) {
				    instance = new Singleton();
			    }
		    }
	    }
	    return instance;
	}
  
	private Singleton() {
	}
}
复制代码

Java中new Singleton()需要经历3步:

  1. 分配内存;
  2. 初始化Singleton对象;
  3. instance指向这块内存。

分析下这3步间的依赖性,分配内存必须最先执行,否则2和3无法进行,至于2和3无论谁先执行,都不会影响单线程下语义的正确性,它们之间不存在依赖性。

但是到了多线程场景下,情况就变得复杂了:

此时线程t2拿到的instance是尚未经过初始化的实例对象,重排序导致的有序性问题就产生了

Tips:拓展中补充指令重排序

JMM都做了什么?

正式描述JMM前,JSR-133中提到了另外两种内存模型:

  • 顺序一致性内存模型
  • Happens-Before内存模型

顺序一致性内存模型禁止了编译器和处理器优化,提供了极强的内存可见性保证。它要求:

  • 执行过程中,所有读/写操作存在全序关系;
  • 线程中的操作必须按照程序的顺序来执行;
  • 操作必须原子执行且立即对所有线程可见。

顺序一致性模型的约束力太强了,显然不适合作为支持并发的编程语言的内存模型。

Happens-Before

Happens-Before描述两个操作结果间的关系,操作A happens-before 操作B(记作A→hbBA \xrightarrow{hb} BAhb​B),即便经过重排序,也应该有操作A的结果对操作B是可见的

Tips:Happens-Before是因果关系,A→hbBA \xrightarrow{hb} BAhb​B是“因”,A的结果对B可见是“果”,执行过程不关我的事。

Happens-Before的规则,我们引用《Java并发编程的艺术》中的翻译:

程序顺序规则:线程中的每个操作happens-before该线程中的任意后续操作。 监视器锁规则:锁的解锁happens-before随后这个锁的加锁。 volatile变量规则:volatile变量的写happens-before后续任意对这个volatile变量的读。 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。 start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。 join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

以上内容出现在JSR-133第5章Happens-Before and Synchronizes-With Edges中,原文较为难读。

这些看似是废话,但是别忘了,我们面对的是多线程环境编译器,硬件的重排序

再次强调,以监视器锁规则为例,虽然只说了解锁发生在加锁前,但实际是解锁后的结果(成功/失败)发生在加锁前。

Tips:Happens-Before可以翻译为发生在...之前,Synchronizes-With可以翻译为与...同步

另外JSR-133还还提及了非volatile变量的规则:

The values that can be seen by a non-volatile read are determined by a rule known as happens-before consistency.

非volatile变量的读操作的可见性又happens-before一致性决定

Happens-Before一致性:存在对变量V的写入操作W和读取操作R,如果满足W→hbRW \xrightarrow{hb} RWhb​R,则操作W的结果对操作R可见(JSR 133上的定义诠释了科学家的严谨)。

JMM虽然不是照单全收Happens-Before的规则(进行了增强),不过还是可以认为:Happens−Before规则≈JMM规则Happens-Before规则 \approx JMM规则Happens−Before规则≈JMM规则。

那么为什么选择Happens-Before呢?实际就是易编程约束性运行效率三者权衡后的结果。

图中只选了今天或多或少提到过的内存模型,其中X86/ARM指的是硬件架构体系。

虽然Happens-Before是JMM的核心,但是除此之外,JMM还屏蔽了硬件间的差异;并为Java开发人员提供了3个并发原语,synchronizedvolatilefinal

拓展内容

关于内存模型和JMM的理论内容已经结束了,这里为文章中出现的概念做个补充,大部分都是硬件层面的内容,不感兴趣的话可以直接跳过了。

缓存一致性协议

缓存一致性协议(Cache Coherence Protocol),一致性用的并不是常见的Consistency。

Coherence和Consistency经常出现在并发编程,编译优化和分布式系统设计中,如果仅仅从中文翻译上理解你很容易误解,实际上两者的区别还是很大的,我们看维基百科中对一致性模型的解释:

Consistency is different from coherence, which occurs in systems that are cached or cache-less, and is consistency of data with respect to all processors. Coherence deals with maintaining a global order in which writes to a single location or single variable are seen by all processors. Consistency deals with the ordering of operations to multiple locations with respect to all processors.

很明显的,如果是Coherence,针对的是单个变量,而Consistency针对的是多个绵连。

MESI协议

MESI协议是基于失效的最常用的缓存一致性协议。MESI代表了缓存的4种状态:

  • M(Modified,已修改),缓存中数据已经被修改,且与主内存数据不同。
  • E(Exclusive,独占),数据只存在于当前核心的缓存中,且与主内存数据相同。
  • S(Shared,共享),数据存在与多个核心中,且与主内存数据相同。
  • I(Invalid,无效),缓存中数据是无效的。

Tips:除了MESI协议外还有MSI协议,MOSI协议,MOESI协议等,首字母都是描述状态的,O代表的是Owned。

MESI是硬件层面做出的保证,它保证一个变量在多个核心上的读写顺序

不同的CPU架构对MESI有不同的实现,如:X86引入了store buffer,ARM中又引入load buffer和invalid queue,读/写缓冲区和无效化队列提高了速度但是带来了另一个问题。

指令重排序

重排序可以分为3类:

  • 指令并行重排序:没有数据依赖的情况下,处理器可以自行优化指令的执行顺序;
  • 编译器优化重排序:不改变单线程语义的前提下,编译器可以重新安排语句的执行顺序;
  • 内存系统重排序:引入store/load buffer,并且异步执行,看起来指令是“乱序”执行的。

前两种重排序很好理解,但是内存系统重排序要怎么理解呢?

引入store buffer,load buffer和invalid queue,将原本同步交互的过程修改为了异步交互,虽然减少了同步阻塞,但也带来了“乱序”的可能性。

当然重排序也不是“百无禁忌”,它有两个底线:

数据依赖

两个操作依赖同一个数据,且其中包含写操作,此时两个操作之间就存在数据依赖。如果两个操作存在数据依赖性,那么在编译器或处理器重排序时,就不能修改这两个操作的顺序

as-if-serial语义

as-if-serial语义并不是说像单线程场景一样执行,而是无论如何重排序,单线程场景下的语义不能被改变(或者说执行结果不变)

推荐阅读

关于内存模型和JMM的阅读资料

  • 什么是内存模型?
  • Java 11语言规范第17章
  • The JSR-133 Cookbook
  • JSR-133(英文版)
  • JSR-133(中文版) 丁一译
  • Time, Clocks, and the Ordering of Events in a Distributed System

虽然《Time, Clocks, and the Ordering of Events in a Distributed System》是讨论分布式领域问题的,但在并发编程领域也有着巨大的影响。

另外,我准备了《Time, Clocks, and the Ordering of Events in a Distributed System》和JSR-133的中英版总计3个PDF文件,回复【JSR133】即可。

最后说个有意思的事情,大佬们的博客都异常“朴素”。

Doug Lea的博客首页:

Lamport的博客首页:

结语

最近沉迷P5R,一直在偷懒~~

JMM的内容删删减减的写得很纠结,因为涉及到并发原理时,从来不是编程语言自己在战斗,从CPU到编程语言每个环节都有参与,所以很难把控每部分内容的详略。

不过好在也是把JMM的本质和由来说明白了,希望这篇对你有所帮助,欢迎各位大佬留言指正。


好了,今天就到这里了,Bye~~

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

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

相关文章

[附源码]计算机毕业设计JAVA社团管理系统

[附源码]计算机毕业设计JAVA社团管理系统 项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis M…

c语言实现三子棋

目录 一、三子棋玩法 二、需要实现的游戏功能 三、拆分代码 3.1游戏菜单 3.2初始化棋盘 3.2.1函数调用 3.2.2函数体实现 3.3打印棋盘 3.3.1函数调用 3.3.2函数体实现 3.4玩家下棋 3.4.1函数调用 3.4.2函数体实现 3.4.3效果展示 3.5电脑下棋 3.5.1函数调用 3.5…

强大博客搭建全过程(1)-hexo博客搭建保姆级教程

1、 前言 本人本来使用国内的开源项目solo搭建了博客,但感觉1核CPU2G内存的服务器,还是稍微有点重,包括服务器内还搭建了数据库。如果自己开发然后搭建,耗费时间又比较多,于是乎开始寻找轻量型的博客系统。 此时hexo…

苹果系统(macos)code with me 控制端下载不下来,下载缓慢,解决办法

jetbrains的插件,依赖包,或者是工具之类的下载通常都比较慢,尤其是大文件等很久后可能还断开了.又要重头下.比如 code with me,以下简称cwm curl: (56) Recv failure: Connection reset by peer 如果你曾用sh安装时提示这个,多数就是网络连接不行了. 简单说就是用下载工具下…

redis集群搭建教程及遇到的问题处理

这里,在一个Linux虚拟机上搭建6个节点的redis伪集群,思路很简单,一台虚拟机上开启6个redis实例,每个redis实例有自己的端口。这样的话,相当于模拟出了6台机器了,然后在以这6个实例组建redis集群就可以了。 …

jsp旅行社管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 旅行社管理系统 是一套完善的web设计系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5 开发,数据库为Mysql,使用…

JAVA之Spring MVC 请求与响应 REST风格 SSM整合(注解版)

SpringMVC是一种基于Java实现MVC模型的轻量级Web框架 使用简单&#xff0c;开发便捷&#xff08;相比于Servlet&#xff09; 灵活性强 入门案例 导入依赖SpringMVC坐标 <dependency><groupId>org.springframework</groupId><artifactId>spring-web…

[附源码]计算机毕业设计JAVA实践教学管理系统

[附源码]计算机毕业设计JAVA实践教学管理系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybati…

01 使用docker搭建wordpress博客网站

文章目录1. 前言1.1 容器技术思维导图1.2 wordpress 网络架构图2. 拉取镜像3. 搭建博客网站3.1 启动MariaDB3.2 运行应用服务器 WordPress3.3 使用nginx 做反向代理3.4 检查容器状态4. 登录博客配置5 查看数据库1. 前言 基于之前学习kubernetes 比较乱&#xff0c;打算重新学习…

机器学习笔记 十九:由浅入深的随机森林模型之分类

随机森林学习内容1. 集成学习2.sklearn中的集成算法2.1 sklearn中的集成算法模块ensemble2.2 RandomForestClassifier2.2.1 参数2.2.2 n_estimators2.2.3 random_state2.2.4 bootstrap & oob_score2.3 随机森林的重要接口2.4 Bonus&#xff08;装袋法的必要条件&#xff09…

【指针详解】(上)看一遍就会❗❗❗家人们冲❗

前言 ❤️ 铁汁们大家好&#xff0c;欢迎大家来到出小月的博客里&#xff0c; &#x1f917;&#x1f917;&#x1f917;之前呢&#xff0c;我分享了C语言的小游戏“扫雷”。。。。今天呢&#xff0c;给大家分享指针篇&#xff0c;&#xff0c;希望大家看完我这篇文章都能够“涨…

Java Tomcat内存马——Servlet内存马

目录 前言&#xff1a; &#xff08;一&#xff09;Servlet的创建 1、实现javax.servlet.Servlet接口的方式 2、继承GenericServlet类创建Servlet 3、继承了HttpServlet进行创建 &#xff08;二&#xff09;分析注入方式 代码分析 (三&#xff09;payload 1、StandardC…

阿里云OSS、用户认证与就诊人

oss 模块搭建与实现 这里采用的方式是通过后端传 oss&#xff0c;可以对比下 谷粒商城里面的&#xff0c;从后端拿上传凭证&#xff0c;然后前端直传的方式 <dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId> <…

数字化管理门店| 甜品店管理系统

对不少女生来说&#xff0c;甜品店绝对算是经常去的地方&#xff0c;尤其出新品的时候&#xff0c;就如奶茶店一样在排队&#xff0c;在武汉一家足球场的旁边&#xff0c;有一家“甜如箭丘”品牌门店&#xff0c;整体环境装修的非常附有文艺感&#xff0c;明亮的店堂&#xff0…

第7章 网络优化与正则化

系列文章目录 第1章 绪论 第2章 机器学习概述 第3章 线性模型 第4章 前馈神经网络 第5章 卷积神经网络 第6章 循环神经网络 第7章 网络优化与正则化 第8章 注意力机制与外部记忆 第9章 无监督学习 第10章 模型独立的学习方式 第11章 概率图模型 第12章 深度信念网络 第13章 深…

[Flask]Pycharm+Flask零基础项目搭建入门

Flask在Python web开发中虽然热度低于Django但是也存在不小的市场空间&#xff0c;能作为一个主流web开发框架之一也不是浪得虚名&#xff0c;还是有不少干货在里面的&#xff0c;Flask也具备了不少的自身优势&#xff0c;在后面的认识中我们再逐步深入了解 今天我们就来带大家…

池式结构:对象池(Object Pool)

本文是对 http://gameprogrammingpatterns.com/object-pool.html 的原创翻译。部分地方使用意译&#xff0c;不准确的地方请各位指证。 一、对象池的意义 通过重新使用固定的池式结构中的对象&#xff0c;来代替单独分配和释放对象&#xff0c;可以提高程序的性能和内存使用。…

裸 VSCode 必备插件

VSCode 轻量、开源&#xff0c;新鲜下载的 VSCode 可谓是身无长物、一穷二白&#xff0c;连个项目管理的功能都没有。 身轻如燕的 VSCode 对于后端开发说可能有点幼稚&#xff0c;但对于前端来说刚刚好&#xff0c;毕竟不需要搞什么 Docker、数据库等等&#xff0c;装俩 VSCod…

02 本机搭建kubernetes学习环境kubemini

文章目录1. 什么是容器编排&#xff1f;2. 什么是 Kubernetes&#xff1f;2.1 Kubernetes 到底能够为我们做什么呢&#xff1f;3. 什么是 minikube4. 如何搭建 minikube 环境4.1 minikube 安装4.2 kubectl 安装5. 验证实验节点5.1 kubectl 使用5.2 在kubernetes 运行第一个应用…

第148篇 笔记-DeFi

定义&#xff1a;去中心化金融(Decentralized finance)&#xff0c;简称“DeFi”&#xff0c;是指基于区块链的无许可和透明金融服务生态系统。 DeFi是区块链、智能合约和预言机带来的最重大进步之一。DeFi一开始是在去中心化基础设施上重新创建通用金融工具的运动&#xff0c…