首页 / 软件开发 / JAVA / Java理论与实践: 修复Java内存模型,第1部分
Java理论与实践: 修复Java内存模型,第1部分2010-12-20 IBM Brian Goetz活跃了将近三年的 JSR 133,近期发布了关于如何修复 Java 内存模型 (Java Memory Model, JMM)的公开建议。原始 JMM 中有几个严重缺陷,这导 致了一些难度高得惊人的概念语义,这些概念原来被认为很简单,如 volatile 、final 以及 synchronized。在这一期的 Java 理论与实践 中,Brian Goetz 展示了如何加强 volatile 和 final 的语义,以修复 JMM。这些更改有些已经 集成在 JDK 1.4 中;而另一些将会包含在 JDK 1.5 中。Java 平台把线程和多处理技术集成到了语言中,这种集成程度比以前的大多 数都要强很多。该语言对于平台独立的并发及多线程技术的支持是野心 勃勃并且是具有开拓性的,或许并不奇怪,这个问题要比 Java 体系结构设计者 的原始构想要稍微困难些。关于同步和线程安全的许多底层混淆是 Java 内存模 型 (JMM)的一些难以直觉到的细微差别,这些差别最初是在 Java Language Specification 的第 17 章中指定的,并且由 JSR 133 重新指定。例如,并不是所有的多处理器系统都表现出 缓存一致性(cache coherency );假如有一个处理器有一个更新了的变量值位于其缓存中,但还没有被存入主 存,这样别的处理器就可能会看不到这个更新的值。在缓存缺乏一致性的情况下 ,两个不同的处理器可以看到在内存中同一位置处有两种不同的值。这听起来不 太可能,但是这却是故意的 —— 这是一种获得较高的性能和可伸缩性的方法 —— 但是这加重了开发者和编译器为解决这些问题而编写代码的负担。什么是内存模型,我为什么需要一个内存模型?内存模型描述的是程序中各变量(实例域、静态域和数组元素)之间的关系 ,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节 。对象最终存储在内存中,但编译器、运行库、处理器或缓存可以有特权定时地 在变量的指定内存位置存入或取出变量值。例如,编译器为了优化一个循环索引 变量,可能会选择把它存储到一个寄存器中,或者缓存会延迟到一个更适合的时 间,才把一个新的变量值存入主存。所有的这些优化是为了帮助实现更高的性能 ,通常这对于用户来说是透明的,但是对多处理系统来说,这些复杂的事情可能 有时会完全显现出来。JMM 允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间 移动的次序拥有重要的特权,除非程序员已经使用 synchronized 或 final 明 确地请求了某些可见性保证。这意味着在缺乏同步的情况下,从不同的线程角度 来看,内存的操作是以不同的次序发生的。与之相对应地,像 C 和 C++ 这些语言就没有显示的内存模型 —— 但 C 语 言程序继承了执行程序处理器的内存模型(尽管一个给定体系结构的编译器可能 知道有关底层处理器的内存模型的一些情况,并且保持一致性的一部分责任也落 到了该编译器的头上)。这意味着并发的 C 语言程序可以在一个,而不能在另 一个,处理器体系结构上正确地运行。虽然一开始 JMM 会有些混乱,但这有个 很大的好处 —— 根据 JMM 而被正确同步的程序能正确地运行在任何支持 Java 的平台上。原始 JMM 的缺点虽然在 Java Language Specification 的第 17 章指定的 JMM 是一个野心 勃勃的尝试,它尝试定义一个一致的、跨平台的内存模型,但它有一些细微而重 要的缺点。 synchronized 和 volatile 的语义很让人混淆,以致于许多有见地 的开发者有时选择忽略这些规则,因为在旧的存储模型下编写正确同步的代码非 常困难。旧的 JMM 允许一些奇怪而混乱的事情发生,比如 final 字段看起来没有那 种设置在构造函数里的值(这样使得想像上的不可变对象并不是不可变的)和内 存操作重新排序的意外结果。这也防止了其他一些有效的编译器优化。如果您阅 读了关于双重检查锁定问题(double-checked locking problem)的任何文章( 参阅 参考资料),您将会记得内存操作重新排序是多么的混乱,以及当您没有 正确地同步(或者没有积极地试图避免同步)时,细微却严重的问题会如何暗藏 在您的代码中。更糟糕的是,许多没有正确同步的程序在某些情况下似乎工作得 很好,例如在轻微的负载下、在单处理器系统上,或者在具有比 JMM 所要求的 更强的内存模型的处理器上。“重新排序”这个术语用于描述几种对内存操作的真实明显的重新排序的类 型:当编译器不会改变程序的语义时,作为一种优化它可以随意地重新排序某些 指令。在某些情况下,可以允许处理器以颠倒的次序执行一些操作。通常允许缓存以与程序写入变量时所不相同的次序把变量存入主存。从另一线程的角度来看,任何这些条件都会引发一些操作以不同于程序指定 的次序发生 —— 并且忽略重新排序的源代码时,内存模型认为所有这些条件都 是同等的。JSR 133 的目标JSR 133 被授权来修复 JMM,它有几个目标:保留现有的安全保证,包括类型安全。提供 无中生有安全性(out-of-thin-air safety)。这意味着变量值并不是 “无中生有”地创建的 —— 所以对于一个线程来说,要观察到一个变量具有变 量值 X,必须有某个线程以前已经真正把变量值 X 写入了那个变量。“正确同步的”程序的语义应该尽可能简单直观。这样,“正确同步的”应 该被正式而直观地定义(这两种定义应该相互一致)。程序员应该要有信心创建多线程程序。当然,我们没有魔法使得编写并发程 序变得很容易,但是我们的目标是为了减轻程序员理解内存模型所有细节的负担 。跨大范围的流行硬件体系结构上的高性能 JVM 实现应该是可能的。现代的处 理器在它们的内存模型上有着很大的不同;JMM 应该能够适合于实际的尽可能多 的体系结构,而不会以牺牲性能为代价。提供一个同步习惯用法(idiom),以允许我们发布一个对象并且使得它不用 同步就可见。这是一种叫做 初始化安全(initialization safety)的新的安全 保证。对现有代码应该只有最小限度的影响。值得注意的是,有漏洞的技术(如双重检查锁定)在新的内存模型下仍然有 漏洞,并且“修复”双重检查锁定技术并不是新内存模型所致力的一个目标。( 但是, volatile 的新语义允许通常所提出的其中一个双重检查锁定的可选方法 正确地工作,尽管我们不鼓励这种技术。)从 JSR 133 process 变得活跃的三年来,人们发现这些问题比他们认为重要 的任何问题都要微妙得多。这就是作为一个开拓者的代价!最终正式的语义比原 来所预料的要复杂得多,实际上它采用了一种与原先预想的完全不同的形式,但 非正式的语义是清晰直观的,将在本文的第 2 部分概要地说明。