BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

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

| 作者 程晓明 关注 71 他的粉丝 发布于 2013年3月16日. 估计阅读时间: 11 分钟 | Google、Facebook、Pinterest、阿里、腾讯 等顶尖技术团队的上百个可供参考的架构实例!

处理器内存模型

顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照。JMM和处理器内存模型在设计时会对顺序一致性模型做一些放松,因为如果完全按照顺序一致性模型来实现处理器和JMM,那么很多的处理器和编译器优化都要被禁止,这对执行性能将会有很大的影响。

根据对不同类型读/写操作组合的执行顺序的放松,可以把常见处理器的内存模型划分为下面几种类型:

  1. 放松程序中写-读操作的顺序,由此产生了total store ordering内存模型(简称为TSO)。
  2. 在前面1的基础上,继续放松程序中写-写操作的顺序,由此产生了partial store order 内存模型(简称为PSO)。
  3. 在前面1和2的基础上,继续放松程序中读-写和读-读操作的顺序,由此产生了relaxed memory order内存模型(简称为RMO)和PowerPC内存模型。

注意,这里处理器对读/写操作的放松,是以两个操作之间不存在数据依赖性为前提的(因为处理器要遵守as-if-serial语义,处理器不会对存在数据依赖性的两个内存操作做重排序)。

下面的表格展示了常见处理器内存模型的细节特征:

内存模型名称

对应的处理器

Store-Load 重排序

Store-Store重排序

Load-Load 和Load-Store重排序

可以更早读取到其它处理器的写

可以更早读取到当前处理器的写

TSO

sparc-TSO

X64

Y

     

Y

PSO

sparc-PSO

Y

Y

   

Y

RMO

ia64

Y

Y

Y

 

Y

PowerPC

PowerPC

Y

Y

Y

Y

Y

在这个表格中,我们可以看到所有处理器内存模型都允许写-读重排序,原因在第一章以说明过:它们都使用了写缓存区,写缓存区可能导致写-读操作重排序。同时,我们可以看到这些处理器内存模型都允许更早读到当前处理器的写,原因同样是因为写缓存区:由于写缓存区仅对当前处理器可见,这个特性导致当前处理器可以比其他处理器先看到临时保存在自己的写缓存区中的写。

上面表格中的各种处理器内存模型,从上到下,模型由强变弱。越是追求性能的处理器,内存模型设计的会越弱。因为这些处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。

由于常见的处理器内存模型比JMM要弱,java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。同时,由于各种处理器内存模型的强弱并不相同,为了在不同的处理器平台向程序员展示一个一致的内存模型,JMM在不同的处理器中需要插入的内存屏障的数量和种类也不相同。下图展示了JMM在不同处理器内存模型中需要插入的内存屏障的示意图:

如上图所示,JMM屏蔽了不同处理器内存模型的差异,它在不同的处理器平台之上为java程序员呈现了一个一致的内存模型。

JMM,处理器内存模型与顺序一致性内存模型之间的关系

JMM是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。下面是语言内存模型,处理器内存模型和顺序一致性内存模型的强弱对比示意图:

从上图我们可以看出:常见的4种处理器内存模型比常用的3中语言内存模型要弱,处理器内存模型和语言内存模型都比顺序一致性内存模型要弱。同处理器内存模型一样,越是追求执行性能的语言,内存模型设计的会越弱。

JMM的设计

从JMM设计者的角度来说,在设计JMM时,需要考虑两个关键因素:

  • 程序员对内存模型的使用。程序员希望内存模型易于理解,易于编程。程序员希望基于一个强内存模型来编写代码。
  • 编译器和处理器对内存模型的实现。编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。编译器和处理器希望实现一个弱内存模型。

由于这两个因素互相矛盾,所以JSR-133专家组在设计JMM时的核心目标就是找到一个好的平衡点:一方面要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能的放松。下面让我们看看JSR-133是如何实现这一目标的。

为了具体说明,请看前面提到过的计算圆面积的示例代码:

double pi  = 3.14;    //A
double r   = 1.0;     //B
double area = pi * r * r; //C

上面计算圆的面积的示例代码存在三个happens- before关系:

  1. A happens- before B;
  2. B happens- before C;
  3. A happens- before C;

由于A happens- before B,happens- before的定义会要求:A操作执行的结果要对B可见,且A操作的执行顺序排在B操作之前。 但是从程序语义的角度来说,对A和B做重排序即不会改变程序的执行结果,也还能提高程序的执行性能(允许这种重排序减少了对编译器和处理器优化的束缚)。也就是说,上面这3个happens- before关系中,虽然2和3是必需要的,但1是不必要的。因此,JMM把happens- before要求禁止的重排序分为了下面两类:

  • 会改变程序执行结果的重排序。
  • 不会改变程序执行结果的重排序。

JMM对这两种不同性质的重排序,采取了不同的策略:

  • 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
  • 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不作要求(JMM允许这种重排序)。

下面是JMM的设计示意图:

从上图可以看出两点:

  • JMM向程序员提供的happens- before规则能满足程序员的需求。JMM的happens- before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证(有些内存可见性保证其实并不一定真实存在,比如上面的A happens- before B)。
  • JMM对编译器和处理器的束缚已经尽可能的少。从上面的分析我们可以看出,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。比如,如果编译器经过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除。再比如,如果编译器经过细致的分析后,认定一个volatile变量仅仅只会被单个线程访问,那么编译器可以把这个volatile变量当作一个普通变量来对待。这些优化既不会改变程序的执行结果,又能提高程序的执行效率。

JMM的内存可见性保证

Java程序的内存可见性保证按程序类型可以分为下列三类:

  1. 单线程程序。单线程程序不会出现内存可见性问题。编译器,runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。
  2. 正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。
  3. 未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。

下图展示了这三类程序在JMM中与在顺序一致性内存模型中的执行结果的异同:

只要多线程程序是正确同步的,JMM保证该程序在任意的处理器平台上的执行结果,与该程序在顺序一致性内存模型中的执行结果一致。

JSR-133对旧内存模型的修补

JSR-133对JDK5之前的旧内存模型的修补主要有两个:

  • 增强volatile的内存语义。旧内存模型允许volatile变量与普通变量重排序。JSR-133严格限制volatile变量与普通变量的重排序,使volatile的写-读和锁的释放-获取具有相同的内存语义。
  • 增强final的内存语义。在旧内存模型中,多次读取同一个final变量的值可能会不相同。为此,JSR-133为final增加了两个重排序规则。现在,final具有了初始化安全性。

参考文献

  1. Computer Architecture: A Quantitative Approach, 4th Edition
  2. Shared memory consistency models: A tutorial
  3. Intel® Itanium® Architecture Software Developer’s Manual Volume 2: System Architecture
  4. Concurrent Programming on Windows
  5. JSR 133 (Java Memory Model) FAQ
  6. The JSR-133 Cookbook for Compiler Writers
  7. Java theory and practice: Fixing the Java Memory Model, Part 2

关于作者

程晓明,Java软件工程师,国家认证的系统分析师、信息项目管理师。专注于并发编程,就职于富士通南大。个人邮箱:asst2003@163.com

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

看完后有两点疑问,请教一下: by Z CS

1. 文中所写的 “未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。”,JSR-133 真的做到这样了么? 那 么 long和double的 读写 的非原子性是怎么回事啊?是不是矛盾啊?

2. 关于本文中最后“JSR-133为final增加了两个重排序规则。现在,final具有了初始化安全性。”,这句话,是不是也还要加上那个 前提“保证final引用不从构造函数内逸出”,final才能具有“初始化安全性”啊?

Re: 看完后有两点疑问,请教一下: by 程 晓明

谢谢您的关注。
******************************************
问题1
最小安全性与64位数据的非原子性读/写并不矛盾。
它们是两个不同的概念,它们“发生”的时间点也不同。

最小安全性保证对象默认初始化之后(设置成员域为0,null或false),才会被任意线程使用。
最小安全性“发生”在对象被任意线程使用之前。

64位数据的非原子性读/写“发生”在对象被多个线程使用的过程中(读/写共享变量)。
当发生《本文三》末尾的那种问题时(处理器B看到仅仅被处理器A“写了一半“的无效值),
这里虽然处理器B读取到一个被写了一半的无效值,但这个值任然是处理器A写入的,只不过处理器A还没有写完而已。

最小安全性保证线程读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。
但最小安全性并不保证线程读取到的共享变量的值,一定是某个线程写完后的值。
最小安全性保证线程读取到的值不会无中生有的冒出来,但并不保证线程读取到的值一定是正确的。
*************************************
问题2
是的,你的理解是对的。
这句话也要加上那个前提:“保证final引用不从构造函数内逸出”,final才能具有“初始化安全性”。

编译器什么时候插入内存屏障? by 黄 春

厚积薄发之做。 明白了很多不解的地方。 这里有个问题想问下。 文中说“java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序”。 这里有个问题请教。
插入内存屏障, 是在编译成字节码的时候完成, 还是在JIT执行的时候重新调整指令生成的? 或者JIT只生产本地代码, 跟插入内存屏障之类的没关系?

Re: 编译器什么时候插入内存屏障? by 程 晓明

Doug Lea在《The JSR-133 Cookbook for Compiler Writers》中,并没有明确指定插入内存屏障的时机。
个人估计,应该是取决于具体的JVM实现。

Re: 编译器什么时候插入内存屏障? by Zhang Gavin

应该是JVM决定何时插入内存屏障。你想,那字节码的格式应该是平台中立的,只有具体到各JVM时,才能确定具体如何插入内存屏障

写得很好 by wu camry

看后明白了许多问题

Re: 编译器什么时候插入内存屏障? by 黄 文海

“java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序”,这个说法有误。插入内存屏障这个动作并不是在字节码这一层中进行的,而是在JIT编译器将字节码转换为机器码(汇编)的过程中实现的。以volatile关键字的处理为例,javac编译器对volatile变量的读、写的处理与普通变量读、写的处理是一致的(即没有插入所谓的内存屏障,这点我们通过javap命令反编译下字节码即可验证)。而在JIT编译器的时候,通过抓取JVM动态生成的汇编代码,我们可以看到JIT编译器在volatile变量写操作之后插入了StoreLoad屏障(在x86处理器上Hotspot VM用的LOCK前缀指令)。那还有个问题就是,JVM也可能是采用解释字节码的方式执行(解释执行),实际上这种执行方式所借助的TemplateTable可以理解为动态将一个字节码“翻译”为相应的机器码。从这个角度来看,解释执行本质上还是编译执行。综上,真正负责在适当地方插入需要的内存屏障这个动作是在将字节码“翻译”为机器码的过程中实现的,而不是将源代码编译为机器码的过程中实现的。也就是说,“The JSR-133 Cookbook for Compiler Writers”中所谓的编译器,并不是指javac,而是JIT动态编译器。

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

7 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT