Java理论与实践: 修复Java内存模子,第1部门
当前位置:以往代写 > JAVA 教程 >Java理论与实践: 修复Java内存模子,第1部门
2019-06-14

Java理论与实践: 修复Java内存模子,第1部门

Java理论与实践: 修复Java内存模子,第1部门

副标题#e#

活泼了快要三年的 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 所要求的 更强的内存模子的处理惩罚器上。

“从头排序”这个术语用于描写几种对内存操纵的真实明明的从头排序的类 型:

当编译器不会改变措施的语义时,作为一种优化它可以随意地从头排序某些 指令。

在某些环境下,可以答允处理惩罚器以颠倒的序次执行一些操纵。

凡是答允缓存以与措施写入变量时所不沟通的序次把变量存入主存。

#p#分页标题#e#

从另一线程的角度来看,任何这些条件城市激发一些操纵以差异于措施指定 的序次产生 —— 而且忽略从头排序的源代码时,内存模子认为所有这些条件都 是同等的。

JSR 133 的方针

JSR 133 被授权来修复 JMM,它有几个方针:

保存现有的安详担保,包罗范例安详。

提供 无中生有安详性(out-of-thin-air safety)。这意味着变量值并不是 “无中生有”地建设的 —— 所以对付一个线程来说,要调查到一个变量具有变 量值 X,必需有某个线程以前已经真正把变量值 X 写入了谁人变量。

“正确同步的”措施的语义应该尽大概简朴直观。这样,“正确同步的”应 该被正式而直观地界说(这两种界说应该彼此一致)。

措施员应该要有信心建设多线程措施。虽然,我们没有邪术使得编写并发程 序变得很容易,可是我们的方针是为了减轻措施员领略内存模子所有细节的承担 。

跨大范畴的风行硬件体系布局上的高机能 JVM 实现应该是大概的。现代的处 理器在它们的内存模子上有着很大的差异;JMM 应该可以或许适合于实际的尽大概多 的体系布局,而不会以牺牲机能为价钱。

提供一个同步习习用法(idiom),以答允我们宣布一个工具而且使得它不消 同步就可见。这是一种叫做 初始化安详(initialization safety)的新的安详 担保。

对现有代码应该只有最小限度的影响。

值得留意的是,有裂痕的技能(如双重查抄锁定)在新的内存模子下仍然有 裂痕,而且“修复”双重查抄锁定技能并不是新内存模子所致力的一个方针。( 可是, volatile 的新语义答允凡是所提出的个中一个双重查抄锁定的可选要领 正确地事情,尽量我们不勉励这种技能。)

从 JSR 133 process 变得活泼的三年来,人们发明这些问题比他们认为重要 的任何问题都要微妙得多。这就是作为一个开辟者的价钱!最终正式的语义比原 来所预料的要巨大得多,实际上它回收了一种与原先预想的完全差异的形式,但 非正式的语义是清晰直观的,将在本文的第 2 部门概腹地说明。


#p#副标题#e#

同步和可见性

大大都措施员都知道, synchronized 要害字强制实施一个互斥锁(相互排 斥),这个互斥锁防备每次有多个线程进入一个给定监控器所掩护的同步语句块 。可是同步尚有另一个方面:正如 JMM 所指定,它强制实施某些内存可见性规 则。它确保了当存在一个同步块时缓存被更新,当输入一个同步块时缓存失效。 因此,在一个由给定监控器掩护的同步块期间,一个线程所写入的值对付其余所 有的执行由同一监控器所掩护的同步块的线程来说是可见的。它也确保了编译器 不会把指令从一个同步块的内部移到外部(固然在某些环境下它会把指令从同步 块的外部移到内部)。JMM 在缺乏同步的环境下不会做这种担保 —— 这就是只 要有多个线程会见沟通的变量时必需利用同步(可能它的同胞,易失性)的原因 。

问题 1:不行变工具不是不行变的

JMM 的个中一个最惊人的缺点是,不行变工具好像可以改变它们的值(这种 工具的稳定性旨在通过利用 final 要害字来获得担保)。(Public Service Reminder:让一个工具的所有字段都为 final 并不必然使得这个工具不行变 — — 所有的字段 还 必需是原语范例或是对不行变工具的引用。)不行变工具( 如 String )被认为不要求同步。可是,因为在将内存写方面的变动从一个线程 流传到另一个线程时存在潜在的延迟,所以有大概存在一种竞态条件,即答允一 个线程首先看到不行变工具的一个值,一段时间之后看到的是一个差异的值。

这是怎么产生的呢?思量到 Sun 1.4 JDK 中 String 的实现,这儿根基上有 三个重要的抉择性字段:对字符数组的引用、长度和描写字符串开始的字符数组 的偏移量。 String 是以这种方法实现的,而不是只有字符数组,因此字符数组 可以在多个 String 和 StringBuffer 工具之间共享,而不需要在每次建设一个 String 时都将文本拷贝到一个新的数组里。譬喻, String.substring() 建设 了一个可以与原始的 String 共享同一个字符数组的新字符串,而且这两个字符 串仅仅只是在长度和偏移量上有所差异。

假设您执行以下的代码:

String s1 = "/usr/tmp";
String s2 = s1.substring(4);  // contains "/tmp"

#p#分页标题#e#

字符串 s2 将具有巨细为 4 的长度和偏移量,可是它将同 s1 共享包括“ /usr /tmp ”的同一字符数组。在 String 结构函数运行之前, Object 的结构 函数将用它们默认的值初始化所有字段,包罗抉择性的长度和偏移字段。当 String 结构器运行时,字符串长度和偏移量被配置成所需要的值。可是在旧的 内存模子下,在缺乏同步的环境下,有大概另一个线程会姑且地看到偏移量字段 具有初默认值 0,尔后又看到正确的值 4。功效是 s2 的值从“ /usr ”酿成了 “ /tmp ”。这并不是我们所想要的,并且在所有 JVM 或平台这是不行能的, 可是旧的内存模子类型答允这样做。

问题 2:从头排序易失性和非易失性存储

另一个主要规模是与 volatile 字段的内存操纵从头排序有关,这个规模中 现有 JMM 引起了一些很是杂乱的功效。现有 JMM 表白易失性的读和写是直接和 主存打交道的,这样制止了把值存储到寄存器可能绕过处理惩罚器特定的缓存。这使 得多个线程一般能瞥见一个给定变量的最新的值。但是,功效是这种 volatile 界说并没有最初所想像的那样有用,而且它导致了 volatile 实际意义上的重大 杂乱。

为了在缺乏同步的环境下提供较好的机能,编译器、运行库缓和存凡是被允 许从头排序普通的内存操纵,只要当前执行的线程判别不出它们的区别。(这就 是所谓的 线程内好像是串行的语义(within-thread as-if-serial semantics )。)可是,易失性的读和写是完全跨线程布置的,编译器或缓存不能在互相之 间从头排序易失性的读和写。遗憾的是,通过参考普通变量的读和写,JMM 答允 易失性的读和写被从头排序,这意味着我们不能利用易失性符号作为操纵已完成 的指示。思量下面的代码,其意图是假定易失性字段 initialized 用于表白初 始化已经完成了。

清单 1. 利用一个易失性字段作为一个“捍卫”变量

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

这里的思想是利用易失性变量 initialized 接受捍卫来表白一套此外操纵已 经完成了。这是一个很好的思想,可是它不能在旧的 JMM 下事情,因为旧的 JMM 答允非易失性的写(好比写到 configOptions 字段,以及写到由 configOptions 引用 Map 的字段中)与易失性的写一起从头排序,因此另一个 线程大概会看到 initialized 为 true,可是对付 configOptions 字段或它所 引用的工具还没有一个一致的可能说当前的视图。 volatile 的旧语义只理睬正 在读和写的变量的可见性,而不理睬其他的变量。固然这种要领更容易有效地实 现,但功效是没有本来所想的那么有用。

竣事语

正如 Java Language Specification 第 17 章中所指定的,JMM 有一些严重 的缺点,即答允一些看起来公道的措施产生一些非直观的或不合需要的工作。如 果正确地编写并发的类太坚苦的话,那么我们可以说很多并发的类不能按预期工 作,而且这是平台中的一个缺点。幸运的是,我们可以在不粉碎在旧的内存模子 下正确同步的任何代码的同时,建设一个与大大都开拓者的直觉越发一致的内存 模子,而且这一切已经过 JSR 133 process 完成。下个月,我们将先容新的内 存模子(它的大部门成果已集成到 1.4 JDK 中)的具体信息。

    关键字:

在线提交作业