首页 / 软件开发 / 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 字段时会 对性能产生更大的影响。