写点什么

OpenJDK修订了Java内存模型

2015 年 7 月 28 日

传统的 Java 内存模型涵盖了很多 Java 语言的语义保证。在这篇文章中,我们将重点介绍其中的几个语义,以更深入地了解他们。对于本文中描述的语义,我们还将尝试体会对现有 Java 内存模型更新的动机。本文中与 JMM 未来更新相关的讨论,将被称为 JMM9。

1. Java 内存模型

现有的 Java 内存模型,如 JSR133(以下称为 JMM-JSR133)中所定义的,为共享内存指定了一致性模型,并且有助于为开发者提供与 JMM-JSR133 表述一致的定义。JMM-JSR133 规范的目标是确保线程通过内存交互语义的精确定义,以便允许优化并提供清晰的编程模型。JMM-JSR133 旨在提供定义和语义,使多线程程序不仅是正确的,而且是高性能的,并对现有代码库的影响微乎其微。

考虑到这一点,我们来过一下 JMM-JSR133 中,过分指定或者指定不足的语义保证,同时重点放到社区广泛讨论的,关于我们如何在 JMM9 对其改进的话题上。

2. JMM9 - 顺序一致性 - 数据竞态自由问题

JMM-JSR133 谈到了相对于操作的程序执行。结合有序操作的执行,描述了这些操作之间的关系。在这篇文章中,我们将扩展一些这样的顺序和关系,进而讨论一下什么是顺序一致的执行。让我们先从“程序顺序”开始。每个线程的程序顺序是一个总体顺序,表示通过该线程执行的所有操作的顺序。有时候,并不是所有操作都需要按序执行的。因此,有一些关系仅是部分有序的关系。例如,happens-before 和 synchronized-with 两个就是部分有序关系。当一个操作发生在另一个操作之前;第一个操作不仅对第二个操作是可见的,而且其顺序在第二个操作之前。这两个操作之间的关系被称为是 happens-before 关系。有时,有些特殊操作需要指定顺序,他们被称为“同步操作”。volatile 的读取和写入、monitor 的锁定和解锁等都是同步操作的例子。一个同步操作会引起该操作的 synchronized-with 关系。synchronized-with 关系是偏序的,这意味着并非所有两两的同步操作都包含这个关系之内。所有同步操作的总体顺序被称为“同步顺序”,每个执行都有一个同步顺序。

现在让我们谈谈顺序一致的执行。当所有的读写操作是总体有序执行时,被认为是顺序一致的(SC)。在 SC 执行中,读操作总是能看到最后一次写入特定变量的值。当 SC 执行表现为没有“数据竞态”时,该程序被认为是数据竞态自由(DRF)的。当程序中有两个不具备 happens-before 关系顺序的访问,他们访问的变量相同且至少其中之一是一个写访问时,就会发生数据竞态。数据竞态自由的顺序一致(SC for DRF)意味着 DRF 程序的行为是顺序一致的。但是严格支持顺序一致是以牺牲性能为代价的,大多数系统会对内存中的操作重新排序,以提高执行速度,并“隐藏”昂贵操作的延迟。同时,编译器也会对代码重新排序以优化执行。在保证严格顺序的一致性的场景中,不能进行这些内存操作重新排序或代码优化,因此性能会受到影响。JMM-JSR133 已经使用底层编译器、高速缓冲存储器的相互作用和对程序不可见的 JIT,合并了松散排序限制和任何重新排序。

注:昂贵操作是那些占用大量的 CPU 周期来完成、阻止执行流水线。

对于 JMM9 来说,性能是一个重要的考虑因素,而且任何一门编程语言的内存模型,理论上,都应该让开发者可以利用内存模型架构上弱有序(weakly-ordered)的优势。成功的实现和示例是放松严格的顺序,尤其是在弱有序的架构上。

注:弱序是指可以对读取和写入重新排序,并且需要显式的内存屏障遏制这种重新排序的架构。

3. JMM9 - 无中生有问题

JMM-JSR133 另一个主要的语义是对“无中生有”(Out-of Thin Air,OoTA)值的禁止。happens-before 模型有时会创建变量值并“无中生有”地读取,因为它不包含因果条件。有一点非常重要,由自身引起的关系不会采用数据和控制依赖的概念,我们将在下面正确同步代码的例子看到,非法写入是由写入本身引起的。

注:x 和 y 初始化为 0) -

Thread a

Thread b

r1 = x;

r2 = y;

if (r1 != 0)

if (r2 != 0)

y = 42;

x = 42;

这段码是 happens-before 一致的,但不是真正的顺序一致。例如,如果 r1 看到为 x=42 的写入,并且 r2 看到 Y=42 的写入,x 和 y 的值都是 42,这是一个数据竞态条件的结果。

r1 = x;

y = 42;

r2 = y;

x = 42;

这里,写入变量都在读取变量之前,读取将看到相关的写入,这将导致 OoTA 结果。

注:数据竞态可能产生推测的结果,这将最终把自己变成自我实现的预言。OoTA 保证是关于秉承因果关系的规则。目前的想法是,因果关系可以避免写入推测。JMM9 旨在寻找 OoTA 的原因和改进方法,以避免 OoTA。

为了禁止 OoTA 值,一些写入需要等待他们的读取来避免数据竞态。因此,JMM-JSR133 定义的 OoTA 禁止正式拒绝 OoTA 读取。这个正式的定义包括内存模型的“执行和因果条件”。基本上,当所有的程序操作提交时,一个良好的执行要满足因果条件。

注:在每次读取可以看到对同一变量的写入时,一个良好的执行遵循在一个线程内、happens-before 和 synchronization-order 一致地执行。

正如你可能已经知道的,JMM-JSR133 定义严格定义,不让 OoTA 值侵袭。JMM9 旨在发现和纠正正式的定义,以便允许一些常见的优化。

4. JMM9 非 Volatile 变量上的 Volatile 操作

首先,关键字’Volatile’是什么意思呢?Java 的 volatile 保证了线程间的交互,使得当一个线程写入一个 volatile 变量,不仅这次写入对其他线程可见,而且其他线程可以看到该线程所有的对 volatile 变量的写入。

那么对于 non-volatile 变量又发生了什么呢?非 volatile 变量没有 volatile 关键字保证交互的好处。因此,编译器可以使用 non-volatile 变量的缓存值而不是 volatile 保证,volatile 变量将总是从内存中读取。happens-before 模型可以用来绑定同步访问到非 volatile 变量上。

注:声明的任何字段为 volatile 并不意味着有锁参与。因此 volatile 比使用锁来同步更便宜。但是着重要注意的是,当方法中有多个 volatile 字段时,可能比使用锁更昂贵。

5. JMM9 - 读写原子性问题和字分裂问题

JMM-JSR133 也有为共享内存并行算法提供的读取和写入的原子性保证(使用异常)。异常是为 non-volatile 的长整型和双精度浮点型的写入被视为两个独立的写入而定义的。因此,一个 64 位的值可以分别写入两个 32 位,一个线程正在执行读的时候,如果其中的一个写入仍未完成,该线程可能会看到只有一半正确的值,从而失去原子性。这是原子性保证依赖于底层硬件和内存子系统的一个例子。例如,底层汇编指令应该能够处理的操作数的大小,以便保证原子性,否则如果读或写操作必须被分成多于一个的操作,最终将破坏原子性(正如例子中的 non-volatile 的长整型和双精度浮点型的值)。类似地,如果因为实现产生一个以上的内存子系统事务,那么也将破坏原子性。

注:volatile 的长整型和双精度浮点型字段和引用始终保证读取和写入的原子性

基于位的设计不是一个理想的解决方案,因为如果 64 位的异常被删除,那么在 32 位的体系结构中就会受损。如果在 64 位架构上行不通,如果期望原子性,那么不得不为长整型和双精度浮点型引入“volatile”,即使底层硬件可以保证原子操作。例如:volatile 类型的字段不需要定义为双精度浮点型,因为基础架构,或者 ISA、浮点单元会处理好 64 位宽字段的原子性需求。JMM9 的目的是确定硬件提供原子性的保证。

JMM-JSR133 写于十多年前 ; 此后处理器位数发生了演变,64 位已经成为主流的处理位数。当即强调的是,JMM-JSR133 提出了针对 64 位读写的妥协,尽管 64 位的值可以由任何架构原子生成,一些架构仍然有必要请求锁。现在,这使得在这些架构上的 64 位读写操作非常昂贵。在 32 位 x86 架构上,如果不能找到一个合理的原子 64 位操作实现,则原子性将不会改变。

注:在语言设计中潜在一个问题,关键字“volatile”被赋予了过分的含义。运行时很难弄清楚,用户使用 volatile 是为了恢复原子性(因此它可以在 64 位平台被剥离出来),还是为了内存排序的目的。

当谈论访问原子性,读写操作的独立性是要着重考虑的。写入一个特定的字段不应该与读取或者写入其他字段有交互。JMM-JSR133 的保证意味着,同步不应需要提供顺序一致性。因此,JMM-JSR133 保证禁止被称为“字分裂”的问题。基本上,当更新一个操作数希望在比基础架构为所有操作数生成的更低的粒度上操作时,我们将遇到“字撕裂”问题。需要记住的重要一点是,字撕裂问题的原因之一是,64 位长整型和双精度浮点型都没有给出原子性保证。字撕裂在 JMM-JSR133 中是禁止的,在 JMM9 中继续保持这种方式。

6. JMM9 - final 字段问题

与其他字段相比,final 字段是不同的。例如,一个线程用 final 字段 x 读取一个“完全初始化”的对象 ; 在对象“完全初始化”后,能保证读取了 final 字段 y 的初始值,但不能保证“正常”的非 final 字段 nonX。

注:“完全初始化”是指对象的构造函数完成。

鉴于上述情况,有一些简单的事情可以在 JMM9 中修复。例如:volatile 类型字段,volatile 字段在构造函数中初始化是不保证可见性的,即使对实例本身是可见的。因此,问题来了,是否 final 字段应该保证扩大到所有字段,包括初始化 volatile 字段?此外,如果一个完全初始化对象的“正常”非 final 字段的值不发生变化,我们是否可以将 final 字段保证到这个“正常”的字段。

参考文献

我从如下这些网站学到了很多,他们提供了大量的示例编码。本文是一篇介绍性的文章,以下文章更适合深入掌握 Java 内存模型。

  1. JSR 133: JavaTM Memory Model and Thread Specification Revision
  2. The Java Memory Model
  3. JAVA CONCURRENCY (&C)
  4. The jmm-dev Archives
  5. Threads and Locks
  6. Synchronization and the Java Memory Model
  7. All Accesses Are Atomic
  8. Java Memory Model Pragmatics (transcript)
  9. Memory Barriers: a Hardware View for Software Hackers

特别感谢

感谢 Jeremy Manson,帮助我纠正了很多误解,并为我更清楚地解释了那些对于我来说很新的术语。还要感谢 Aleksey Shipilev,帮助我减少了本文草稿版本中出现的概念的复杂性。Aleksey 还指导我们去他的 JMM,语用学文章更深层次的理解,澄清和例子。

关于作者

Monica Beckwith是 Java 性能顾问。她过去曾经与 Oracle/Sun 和 AMD 一起工作,对 JVM 服务器级系统进行优化。Monica 被评为 JavaOne 2013 的明星演讲者,并且是 First Garbage Collector(G1 GC)性能团队的领导者。她的 Twitter 是 @mon_beck。

查看英文原文: The OpenJDK Revised Java Memory Model


感谢张龙对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

2015 年 7 月 28 日 00:073583

评论

发布
暂无评论
  • Oracle 确定 Unsafe 类处理策略

    在上周的一篇博文中,Oracle明确了sun.misc.Unsafe类的一些方向(来自不受支持的sun.misc包)。关于这个讨论中的问题,现在需要关注的是,这个应用广泛的类将来仅限于通过Jigsaw项目的JDK模块来访问。

  • 与 Neal Gafter 探讨 Java 的未来

    Neal Gafter讨论了Oracle的收购对Java的影响,以及为Java增加分段式栈和元对象协议的情况,并与C#/.NET做了比较。

  • Lock 和 Condition(上):隐藏在并发包中的管程

    Java SDK并发包通过Lock和Condition两个接口来实现管程,分别解决互斥和同步问题。

    2019 年 3 月 30 日

  • 深入理解 Java 内存模型(三)——顺序一致性

    Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,本文试图揭开Java内存模型神秘的面纱。本文大致分三部分:重排序与顺序一致性;三个同步原语(lock,volatile,final)的内存语义,重排序规则及在处理器中的实现;Java内存模型的设计目标,及其与处理器内存模型和顺序一致性内存模型的关系。

  • Java 内存模型

    Java内存模型通过定义了一系列的happens-before操作,让应用程序开发者能够轻易地表达不同线程的操作之间的内存可见性。

    2018 年 8 月 20 日

  • 深入理解 Java 内存模型(七)——总结

    Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,本文试图揭开Java内存模型神秘的面纱。本文大致分三部分:重排序与顺序一致性;三个同步原语(lock,volatile,final)的内存语义,重排序规则及在处理器中的实现;Java内存模型的设计目标,及其与处理器内存模型和顺序一致性内存模型的关系。

  • 深入理解 Java 内存模型(一)——基础

    Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,本文试图揭开Java内存模型神秘的面纱。本文大致分三部分:重排序与顺序一致性;三个同步原语(lock,volatile,final)的内存语义,重排序规则及在处理器中的实现;Java内存模型的设计目标,及其与处理器内存模型和顺序一致性内存模型的关系。

  • 如何用硬件同步原语(CAS)替代锁?

    在特定的场景中,CAS原语可以替代锁,在保证安全性的同时,提供比锁更好的性能。

    2019 年 9 月 3 日

  • 多线程调优(下):如何优化多线程上下文切换?

    你有没有想过将上下文切换作为系统的性能参考指标,并纳入到服务性能监控中呢?

    2019 年 6 月 25 日

  • 软件性事务:一种编程语言的视角

    Erlang,一种可以让并发兼得优美与效率的语言,最近受到了很大的关注。特别是在“进程”实例之间没有共享存储,它们的通信只能依靠异步消息。然而,共享存储形式的并发仍然是一个强势的研究课题,尤其在多核应用领域更是如此。

  • 在堆增大的同时确保垃圾回收停顿时间短暂——专访 Cliff Click 博士

    堆大小和垃圾回收停顿时间之间的强相关性成为Java应用程序伸缩性的主要限制之一,专家进行了大量研究以努力改进这种情况。InfoQ与HotSpot Server编译器的前架构师和首席程序员、现任Azul Systems公司首席JVM架构师的Cliff Click博士讨论了Azul的解决方案。

  • Java 深度历险(三)——Java 线程​:基本概念、可见性与同步

    对于Java来说,在语言内部提供了线程的支持。但是Java的多线程应用开发会遇到很多问题。首先是很难编写正确,其次是很难测试是否正确,最后是出现问题时很难调试。一个多线程应用可能运行了好几天都没问题,然后突然就出现了问题,之后却又无法再次重现出来。如果在正确性之外,还需要考虑应用的吞吐量和性能优化的话,就会更加复杂。本文主要介绍Java中的线程的基本概念、可见性和线程同步相关的内容。

  • Java 6 中的线程优化真的有效么?

    像偏向锁、锁粗化、通过逸出分析的锁省略以及自适应的自旋锁等技术,都是为了提高并发性而出现的。它们允许应用程序线程之间可以更多更高效地共享数据。但是它们真的有效么?在这篇由两部分组成的文章里,Jeroen Borgers将逐一探究这些特性,并尝试在单一线程基准的协助下,回答关于性能的问题。

  • 线程本地存储模式:没有共享,就没有伤害

    线程本地存储模式本质上是一种避免共享的方案。没有共享,自然也就没有并发问题。

    2019 年 5 月 7 日

  • 关于实时 Java 的系列文章

    Sun开发者网络登载了一篇关于实时Java系统的文章。该文章由两部分组成,内容覆盖线程、内存和垃圾回收等问题,并对Sun Java RTS平台进行了介绍。

  • 05| RWMutex:读写锁的实现原理及避坑指南

    我会给你介绍读写锁的使用场景、实现原理以及容易掉入的坑,你一定要记住这些陷阱,避免在实际的开发中犯相同的错误。

    2020 年 10 月 21 日

  • 深入理解 Java 内存模型(二)——重排序

    Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,本文试图揭开Java内存模型神秘的面纱。本文大致分三部分:重排序与顺序一致性;三个同步原语(lock,volatile,final)的内存语义,重排序规则及在处理器中的实现;Java内存模型的设计目标,及其与处理器内存模型和顺序一致性内存模型的关系。

  • 聊聊并发(一)——深入分析 Volatile 的实现原理

    在Java多线程并发编程中,synchronized和Volatile都扮演着重要的角色,Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。本文将深入分析在硬件层面上Inter处理器是如何实现Volatile的,通过深入分析能帮助我们正确的使用Volatile变量。

发现更多内容

架构师训练营学习心得初谈

潜默闻雨

小白学软件架构

鸠摩智

架构 UML

架构总结

高高

食堂就餐系统架构设计

K先生

架构师训练 - 20200610 - 学习总结

lei Shi

作业一:食堂就餐卡系统设计

叶荣添CANADA

极客大学架构师训练营

作业一:食堂就餐卡系统设计

Melo

极客大学架构师训练营

第一周学习总结

Jeremy

餐卡管理系统关键设计图

lei Shi

架构师训练营第一周总结作业

兔狲

极客大学架构师训练营

作业一:食堂就餐卡系统设计

金桔🍊

极客大学架构师训练营

【架构师训练营-Week-1】总结

Andy

「架构师训练营」食堂就餐卡系统设计-week1

隆隆

食堂就餐系统设计

石印掌纹

UML

朋友,您可能是MCR的受害者

newbe36524

Docker Dockerfile .net core

第一周作业

lwy

Week01 食堂就餐卡系统设计

极客大学架构师训练营

作业二:根据当周学习情况,完成一篇学习总结

叶荣添CANADA

构架师训练营第一周 作业一:食堂就餐卡系统设计

孙有能希

食堂就餐卡设计

吴吴

成为一名架构师

谭焜鹏

就餐卡系统设计

stars

第一周作业一:食堂就餐卡系统设计

iHai

架构是训练营

架构学习第一周总结

云峰

架构师如何做架构

小遵

架构学习第一周学习总结

乐天

常见的几种广告形式以及 OTT 广告与在线广告区别

子悠

计算广告 互联网广告

架构师训练营第0期第一周总结

陌生人

2020-06-10-第一周学习总结

路易斯李李李

架构师训练营第1周-心得体会

Larry

食堂就餐卡系统设计

Vincent

极客时间

OpenJDK修订了Java内存模型-InfoQ