Welcome 微信登录

首页 / 软件开发 / JAVA / Java理论与实践: 修复Java内存模型,第2部分

Java理论与实践: 修复Java内存模型,第2部分2010-12-20 IBM Brian Goetz活跃了将近三年的 JSR 133,近期发布了关于如何修复 Java 内存模型 (Java Memory Model, JMM)的公开建议。在本系列文章的 第 1 部分,专栏作 者 Brian Goetz 主要介绍最初的 JMM 中的几个严重缺陷,这些缺陷导致了一些 难度高得惊人的概念语义,这些概念原来被认为很简单。这个月,他介绍在新 JMM 中 volatile 和 final 的语义是如何变化的,这些改变使它们的语义符合 大多数开发人员的直觉。其中一些改变已经在 JDK 1.4 中出现了,另一些改变 则要等到 JDK 1.5。请您在本文的讨论论坛上与作者及其他读者交流您的想法。

开始编写并发代码是一件困难的事情,语言不应当增加它的难度。虽然 Java 平台从一开始就包括了对线程的支持,包括一个计划为正确同步的程序提供“一 次编写,到处运行”保证的、跨平台的内存模型,但是原来的内存模型有一些漏 洞。虽然许多 Java 平台提供了比 JMM 所要求的更强的保证,但是 JMM 中的漏 洞使得无法容易地编写可以在任何平台上运行的并发 Java 程序。所以在 2001 年 5 月,成立了以修复 Java 内存模型为目的的 JSR 133。 上个月,我讨论了 其中一些漏洞,这个月,我们将讨论如何堵住它们。

修复后的可见性

理解 JMM 所需要的一个关键概念是 可见性(visibility)——如何知道当 线程 A 执行 someVariable?=?3 时,其他线程是否可以看到线程 A 所写的值 3 ?有一些原因使其他线程不能立即看到 someVariable 的值 3:可能是因为编译 器为了执行效率更高而重新排序了指令,也可能是 someVariable 缓存在寄存器 中,或者它的值写到写处理器的缓存中、但是还没有刷新到主存中,或者在读处 理器的缓存中有一个老的(或者无效的)值。内存模型决定什么时候一个线程可 以可靠地“看到”由其他线程对变量的写入。特别是,内存模型定义了保证内存 操作跨线程的可见性的 volatile 、 synchronized 和 final 的语义。

当线程为释放相关监视器而退出一个同步块时,JMM 要求本地处理器缓冲刷 新到主存中。(实际上,内存模型不涉及缓存——它涉及一个抽象( 本地内存 ), 它包围了缓存、注册表和其他硬件和编译优化。)与此类似,作为获得监视 的一部分,当进入一个同步块时,本地缓存失效,使之后的读操作直接进入主内 存而不是本地缓存。这一过程保证当变量是由一个线程在由给定监视器保护的同 步块中写入,并由另一个线程在由同一监视器保护的同步块中读取时,对变量的 写可被读线程看到。如果没有同步,则 JMM 不提供这种保证——这就是为什么 在多个线程访问同一个变量时,必须使用同步(或者它的更年轻的同胞 volatile )。

对 volatile 的新保证

volatile 原来的语义只保证 volatile 字段的读写直接在主存而不是寄存器 或者本地处理器缓存中进行,并且代表线程对 volatile 变量进行的这些操作是 按线程要求的顺序进行的。换句话说,这意味着老的内存模型只保证正在读或写 的变量的可见性,不保证写入其他变量的可见性。虽然可以容易实现它,但是它 没有像最初设想的那么有用。

虽然对 volatile 变量的读和写不能与对其他 volatile 变量的读和写一起 重新排序,但是它们仍然可以与对 nonvolatile 变量的读写一起重新排序。在 第 1 部分 中,介绍了清单 1 的代码(在旧的内存模型中)是如何不足以保证 线程 B 看到 configOptions 及通过 configOptions 间接可及的所有变量(如 Map 元素)的正确值,因为 configOptions 的初始化可能已经随 volatile initialized 变量进行重新排序。

清单 1. 用一个 volatile 变量作为“守护”

Map configOptions;
char[] configText;
volatile boolean initialized = false;
// In Thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
// In Thread B
while (!initialized)
sleep();
// use configOptions

不幸地,这是 volatile 常见用例——用一个 volatile 字段作为“守护” 表明已经初始化了一组共享变量。JSR 133 Expert Group 决定让 volatile 读 写不能与其他内存操作一起重新排序是有意义的——可以准确地支持这种和其他 类似的用例。在新的内存模型下,如果当线程 A 写入 volatile 变量 V 而线程 B 读取 V 时,那么在写入 V 时,A 可见的所有变量值现在都可以保证对 B 是 可见的。结果就是作用更大的 volatile 语义,代价是访问 volatile 字段时会 对性能产生更大的影响。