全网最硬核 Java 新内存模型解析与实验 - 2. 原子访问与字分裂

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 全网最硬核 Java 新内存模型解析与实验 - 2. 原子访问与字分裂
个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~本篇文章参考了大量文章,文档以及论文,但是这块东西真的很繁杂,我的水平有限,可能理解的也不到位,如有异议欢迎留言提出。 本系列会不断更新,结合大家的问题以及这里的错误和疏漏,欢迎大家留言如果你喜欢单篇版,请访问: 全网最硬核 Java 新内存模型解析与实验单篇版(不断更新QA中)如果你喜欢这个拆分的版本,这里是目录:


JMM 相关文档:


内存屏障,CPU 与内存模型相关:


x86 CPU 相关资料:


ARM CPU 相关资料:


各种一致性的理解:


Aleskey 大神的 JMM 讲解:


相信很多 Java 开发,都使用了 Java 的各种并发同步机制,例如 volatile,synchronized 以及 Lock 等等。也有很多人读过 JSR 第十七章 Threads and Locks(地址:https://docs.oracle.com/javase/specs/jls/se17/html/jls-17.html),其中包括同步、Wait/Notify、Sleep & Yield 以及内存模型等等做了很多规范讲解。但是也相信大多数人和我一样,第一次读的时候,感觉就是在看热闹,看完了只是知道他是这么规定的,但是为啥要这么规定,不这么规定会怎么样,并没有很清晰的认识。同时,结合 Hotspot 的实现,以及针对 Hotspot 的源码的解读,我们甚至还会发现,由于 javac 的静态代码编译优化以及 C1、C2 的 JIT 编译优化,导致最后代码的表现与我们的从规范上理解出代码可能的表现是不太一致的。并且,这种不一致,导致我们在学习 Java 内存模型(JMM,Java Memory Model),理解 Java 内存模型设计的时候,如果想通过实际的代码去试,结果是与自己本来可能正确的理解被带偏了,导致误解。

我本人也是不断地尝试理解 Java 内存模型,重读 JLS 以及各路大神的分析。这个系列,会梳理我个人在阅读这些规范以及分析还有通过 jcstress 做的一些实验而得出的一些理解,希望对于大家对 Java 9 之后的 Java 内存模型以及 API 抽象的理解有所帮助。但是,还是强调一点,内存模型的设计,出发点是让大家可以不用关心底层而抽象出来的一些设计,涉及的东西很多,我的水平有限,可能理解的也不到位,我会尽量把每一个论点的论据以及参考都摆出来,请大家不要完全相信这里的所有观点,如果有任何异议欢迎带着具体的实例反驳并留言


3. 原子性访问


原子性访问,对于一个字段的写入与读取,这个操作本身是原子的不可分割的。可能大家不经常关注的一点是根据 JLS 第 17 章中的说明,下面这两个操作,并不是原子性访问的:


image.png


因为大家当前的系统通常都是 64 位的,得益于此,这两个操作大多是原子性的了。但是其实根据 Java 的规范,这两个并不是原子性的,在 32 位的系统上就保证不了原子性。我这里直接引用 JLS 第 17 章的一段原话:

For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.Writes and reads of volatile long and double values are always atomic.

翻译过来,简单来说非 volatile 的 long 或者 double 可能会按照两次单独的 32 位写更新,所以是非原子性的。volatile 的 long 或者 double 读取和写入都是原子性的。

为了说明我们这里的原子性,我引用一个 jcstress 中的一个例子:


image.png


我们使用 Java 8 32bit (Java 9 之后就不再支持 32 位的机器了)的 JVM 运行这里的代码,结果是:


image.png


可以看到,结果不止 -1 和 0 这种我们代码中的指定的值,还有一些中间结果。


4. 字分裂(word tearing)


字分裂(word tearing)即你更新一个字段,数组中的一个元素,会影响到另一个字段,数组中的另一个元素的值。例如处理器没有提供写单个 byte 的功能,假设最小维度是 int,在这样的处理器上更新 byte 数组,若只是简单地读取 byte 所在的整个 int,更新对应的 byte,然后将整个 int 再写回,这种做法是有问题的。Java 中没有字分裂现象,字段之间以及数组元素之间是独立的,更新一个字段或元素不能影响任何其它字段或元素的读取与更新。


为了说明什么是字分裂,举一个不太恰当的例子,即线程不安全的 BitSet。BitSet 的抽象是比特位集合(一个一个 0,1 这样,可以理解为一个 boolean 集合),底层实现是一个 long 数组,一个 long 保存 64 个比特位,每次更新都是读取这个 long 然后通过位运算更新对应的比特位,再更新回去。接口层面是一位一位更新,但是底层却是按照 long 的维度更新的(因为是底层 long 数组),很明显,如果没有同步锁,并发访问就会并发安全问题从而造成字分裂的问题:


image.png


结果是:


image.png


这里用了一个不太恰当的例子来说明什么是字分裂,Java 中是可以保证没有字分裂的,对应上面的 BitSet 的例子就是我们尝试更新一个 boolean 数组,这样结果就只会是 true true:


image.png


这个结果只会是 true true

接下来,我们将进入一个比较痛苦的章节了,内存屏障,不过大家也不用太担心,从我个人的经验来看,内存屏障很难理解的原因是因为网上基本上不会从 Java 已经为你屏蔽的底层细节去给你讲,直接理解会很难说服自己,于是就会猜想一些东西然后造成误解,所以本文不会上来丢给你 Doug Lea 抽象的并一直沿用至今的 Java 四种内存屏障(就是 LoadLoad,StoreStore,LoadStore 和 StoreLoad 这四个,其实通过后面的分析也能看出来,这四个内存屏障的设计对于现在的 CPU 来说已经有些过时了,现在用的更多的是 acquire, release 以及 fence)希望能通过笔者看的一些关于底层细节的文章论文中提取出便于大家理解的东西供大家参考,更好地更容易的理解内存屏障。

相关文章
|
9天前
|
存储 缓存 Java
java线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理
|
8天前
|
安全 Java 编译器
java访问字段
java访问字段
19 6
|
6天前
|
安全 Java 编译器
java访问类字段
java访问类字段
|
4天前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
13 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
6天前
|
安全 Java 开发者
Java修饰符与封装:理解访问权限、行为控制与数据隐藏的重要性
Java中的修饰符和封装概念是构建健壯、易维护和扩展的Java应用程序的基石。通过合理利用访问权限修饰符和非访问修饰符,开发者能够设计出更加安全、灵活且高效的代码结构。封装不仅是面向对象编程的核心原则之一,也是提高软件项目质量和可维护性的关键策略。
10 1
|
7天前
|
监控 算法 Java
深入解析Java中的垃圾回收机制
本文旨在全面解析Java的垃圾回收机制,探讨其工作原理、常见算法以及在实际开发中的应用。通过对这一重要主题的深入分析,希望帮助读者更好地理解Java虚拟机(JVM)如何管理内存,从而编写出更高效、稳定的Java应用程序。
|
7天前
|
Java 开发者
Java中的异常处理机制深度解析
在Java编程中,异常处理是保证程序稳定性和健壮性的重要手段。本文将深入探讨Java的异常处理机制,包括异常的分类、捕获与处理、自定义异常以及一些最佳实践。通过详细讲解和代码示例,帮助读者更好地理解和应用这一机制,提升代码质量。
12 1
|
3天前
|
存储 缓存 NoSQL
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
10 0
|
8天前
|
分布式计算 Java API
深入解析Java中的Lambda表达式及其应用
本文将深入探讨Java中Lambda表达式的定义、优势及其在实际编程中的应用。通过具体示例,帮助读者更好地理解和使用这一强大的编程工具。
|
2月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。

推荐镜像

更多
下一篇
无影云桌面