86 81 第五章 极简同步技巧 在前面两章中,我们讨论过让对象具有 thread安全性的方法,能够让它们在同一时间在 两个或两个以上的thread中使用。thread的安全性是好的thread程序设计中最重要的项 目;race condition 是非常难以再生与改正的。 在本章中,我们会通过两个相关的议题完成对数据同步与 thread安全性的讨论。开始是 Java内存模型的讨论,它定义了变量实际是如何被 thread访问。这个模型有着一些令人 惊讶的效果。我们会对前面章节澄清的议题之一就是何谓thread被模型化成指令的列表。 在解释完内存模型之后,我们会讨论到 volatile 变量如何融入它以及为何它们在多个 thread 间能够被安全地使用。这些议题全部都与避免同步有关。 随后我们会讨论数据同步的其他途径:使用 atomic 的 class。这一组在 J2SE 5.0 引进的 class能够让特定类型的数据于特定的操作上被定义为atomic的。这些 class在操作上提 供了一个相当好的数据抽象概念,同时也会防止可能由操作本身所引发出的 race condition。这些class在采用不同方式以实现同步这方面也很有意思:相较于明确地将数 据访问同步化,它们使用了一种能够允许 race condition 的发生方式,但又确保 race condition 都是良性的。因此,这些 class 能够自动地避免明确的同步。 能避免同步吗? threaded程序的开发者通常都对同步有妄想症状。有许多关于因为过度或不正确的同步 导致程序性能很差的恐怖故事在坊间流传。如果对某特定的 lock有非常多的竞争,取得 该 lock 会因为下面两个因素而让成本非常的高昂: • 在许多 virtual machine 的实现中取得有竞争与无竞争 lock 的程序编写方式是不相 87极简同步技巧 82 同的。取得有竞争的lock需要在virtual machine的层次上运行更多的程序代码。无 论如何,相反的说法也是对的:取得无竞争的 lock 是相当廉价的操作。 • 在有竞争的 lock 能被取得之前,现有的持有者必须先将其释放。要取得有竞争的 lock 的 thread 总是必须等到 lock 被释放后。 有竞争与无竞争的 Lock 有竞争的(contended)与无竞争的(uncontended)这两个术语是指有多少的thread 在某特定的 lock 上操作。没有被任何 thread持有的 lock 是无竞争的 lock:第一个 试着要取得它的 thread 会立即地成功。 当某个 thread尝试要取得一个已经被其他 thread 持有的 lock 时,这个 lock就变成 了有竞争的 lock。一个有竞争的 lock 至少有一个 thread 在等着撷取它,也有可能 是很多个。注意,有竞争的 lock 会于 thread 不再要等待取得它时变成个无竞争的 lock。 实际上,上述的第二条是最显著的:如果其他人占有了 lock,你就必须等它,这会让程 序的性能大幅地下降。我们会在第十四章讨论到与 thread 性能有关的操作。 这个情况会让程序设计师想尝试在程序中限制同步。这是个不错的主意,你当然会比出 现无谓的运算还更不想见到无谓的同步出现在程序中。但是否有办法能够完全地避掉同 步? 我们已经看过在某种情况下这个答案可以是“ Yes”:你可以对 instance 变量使用 volatile关键字。这些变量无法不被完整地存储,所以在读取它们的时候,你知道你是 在读取有效的值 —— 最近一次被存入此变量的值。在本章稍后部分,我们会看到其他 能够以特定的 class 来让无同步数据访问也能够被接受的例子。 但这些是可以避免同步的唯一的情形。在其他的状况中,如果有多个 thread访问相同的 一组数据,你必须明确地同步化所有对该数据的访问以防止各种的 race condition。 这个原因与计算机将程序最佳化有关系。计算机会执行两种主要的最佳化动作:创建寄 存器来保持数据与重排( reordering)语句。 寄存器的效应 计算机有一定数量的主存储器用来存储与程序有关的数据。在声明一个变量的时候(如 第五章88 83 在数个 class中都用到的 done标记),计算机在一旁设置一个特定的存储单元来保存该 变量的值。 大多数的 CPU 都能够直接地操作保存在主存储器中的数据。其他的 CPU 则只能读写主 存储单元,这些计算机必须将数据从主存储器中读到寄存器中,对寄存器操作,然后将 数据存回存储器。即使是能够对主存储器中的数据直接操作的CPU通常也有一组可以保 存数据的寄存器,且对寄存器中的数据操作通常比对主存储器中的数据操作要快上许多。 因此,在计算机运行程序代码时,到处都在使用寄存器。 从逻辑的观点来看,每个 thread都有自己的一组寄存器。当操作系统将某 thread分配给 CPU 时,它会把该 thread 特有的信息加载到 CPU 的寄存器中;在分配不同的 thread 给 CPU之前,它会将寄存器的信息存下来。所以thread间决不会共享保存在寄存器的数据。 让我们来看一下这是如何应用在Java程序上。在终结一个 thread的时候,我们通常会使 用 done 标记。此 thread(或 runnable 对象)带有像下面这样的程序代码: public void run() { while (!done) { foo(); } } public void setDone() { done = true; } 假设说我们是这样声明 done: private boolean done = false; 这会将某个特定的存储单元(例如说 0xff12345)与 done变量结合起来,并将此存储单 元的值设为 0(false 值在机器上的表示法)。 然后 run()这个 method 会被编译成一组指令: Begin method run Load register r1 with memory location 0Xff12345 Label L1: Test if register r1 == 1 If true branch to L2 Call method foo Branch to L1 Label L2: End method run 与此同时,setDone()方法看起来应该是下面这样: 89极简同步技巧 Begin method setDone Store 1 into memory location 0xff12345 End method setDone 问题在这里:run()方法绝不会将存储单元0xff12345的内容重新加载到寄存器 r1。因 此 run()这个 method 永远不会停止。 然而,假设我们将 done 这样定义: private volatile boolean done = false; 现在 run()方法在逻辑上看起来应该是这样: Begin method run Label L1: Test if memory location 0xff12345 == 1 If true branch to L2 Call method foo Branch to L1 Label L2: End method 使用volatile 关键字能够确保变量不会保持在寄存器中。这能够保证变量是真正地共 享于 thread 之间( 注 1)。 还记得我们可能会将此程序代码实现成同步化对 done 标记的访问(而不是设定 done 标记为 volatile)。这之所以可行是因为同步边界标示出 virtual machine必须要将它 的寄存器视为无效的。当 virtual machine 进入 synchronized method 或块的时候,它必 须重新加载本来已经cached到自有寄存器上的数据。在virtual machine离开同步method 或块值之前,它必须把自有寄存器存入主存储器中。 重排语句的效应 开发者经常希望可通过改变语句的执行顺序来避开同步。假设我们决定要记录打字游戏 在数个回合间的总分,resetScore()这个 method 就有可能会被写成下面这样: public int currentScore, totalScore, finalScore public void resetScore(boolean done) { totalScore += currentScore; if (done) { 注 1: 只要不违反我们所要求的语义,virtual machine 还是可以用寄存器来处理volatile 变量。 这只是个必须遵守的原则,而不是实际操作的规范。 84 第五章90 finalScore = totalScore; currentScore = 0; } } public int getFinalScore() { if (currentScore == 0) return finalScore; return -1; } 这会引发 race condition,因为 thread t1 与 thread t2 的执行顺序可能会是这样: Thread1: Update total score Thread2: See if currentScore == 0 Thread2: Return -1 Thread1: Update finalScore Thread1: Set currentScore == 0 这不是程序逻辑必然的结果。如果周期性地检查分数,这一次会得到- 1,但下一次会得 到正确得结果。依据程序的需求,这可能是非常可以接受的。 然而,你无法依赖像这样排列过的执行顺序。 virtual machine可能会决定在它分配最终 结果前将0 存储于currentScore是更有效率的。这样的决策是在运行期间根据运行程 序的硬件所做出的。在这种情形下,程序运行的是这个次序: Thread1: Update total score Thread1: Set currentScore == 0 Thread2: See if currentScore == 0 Thread2: Return finalScore Thread1: Update finalScore 现在 race condition 就会引发一个问题:返回错误的最终分数。注意,不管变量是否被 定义为volatile 都没有差别,包含 volatile 变量的语句也会跟其他语句一样被重新排 过。 此处唯一能够帮上忙的就是同步。如果 resetScore()与 getFinalScore()方法是被 synchronize 过的,method 中的语句是否被重排过就不重要了,因为同步能够防止执行 method 的 thread 交错。 synchronized块同样也能够防止语句的重排。virtual machine不能将语句从synchronized 块移动到 synchronized块之外。然而,要注意反过来说就不对了:在 synchronized块之 前的语句可以被移动到块内,且在 synchronized 块后面的语句也可以被移动到块内。 85 91极简同步技巧 双重检查的 Locking 这个 design pattern 在第一次被提出时受到相当的重视,不过它现在已经完全地被唾弃 了。但它还是偶尔会被拿出来,所以在此将细节提出来以满足读者的好奇心。 有一个例子是开发者尝试以lazy initialization来避开同步。在这个范例中,有个带有引 用、要花时间构造的对象,所以开发者将对象的构造给延后了: Foo foo; public void useFoo() { if (foo == null) { synchronized(this) { if (foo == null) foo = new Foo(); } } foo.invoke(); } 在此处开发者的目的是要在一旦foo对象被初始化时防止同步。很不幸地, 这个pattern 并不行,原因是因为我们刚刚所看过的那个问题。特别是 foo 的值可以在 foo 的 constructor 被调用前就被存储,之后另一个 thread 进入 useFoo()方法会在 foo 的 constructor 完成之前调用foo.invoke()方法。假使 foo 是个 volatile primitive(但 不是个primitive对象),这还算能正常运行,只要你不在乎foo会被初始化一次以上(且 对 foo 多次的初始化也保证会产出相同的值)。 想要知道双重检查( double-checked)的 locking 这个 pattern以及 Java 内存模型的更多 内容,请见 http://www.cs.umd.edu/~pugh/java/memoryModel/。 Atomic 变量 同步的目的是防止 race condition 导致数据在不一致或变动中间状态被使用到。多个 thread会在程序段由同步保护的期间被禁止竞争。这并不意味着产出或者 thread的执行 的顺序是命中注定的:thread间可能在同步的程序代码段运行之前就开始竞争了。且如 果 thread正在等待相同的同步lock,thread执行 synchronized 程序代码的顺序是由lock 的授予(一般来说这是由平台所决定且结果是非注定的)来决定的。 这是微妙但又重要的一点:并不是所有的 race condition 都应该避免,只有在无 thread 安全性的程序段中的 race condition 才会被认为是个问题。我们可以使用两种办法中的 任一种来改正这个问题:可以用 synchronized程序代码来防止race condition的发生,或 是将程序设计成无需同步(或仅使用最少的同步)就具有 thread 安全性。 86 第五章92 我们相信两种技术你都曾试过。在第二种情形中,尽可能缩小同步的范围并重新组织程 序代码以便让有 thread 安全性的段落能够被移出 synchronized 块之外,这是很重要的。 使用 volatile 变量又是另一个例子。如果有足够多的程序代码能被移出 synchronized 块 之外,也就不需要做同步化了。 这意味着在同步与 volatile 变量之间存在平衡关系。依据程序的算法来决定可以使用两 种技巧中的哪一种并不重要,事实上是可以在设计程序时两种都用。当然,这个平衡是 倾向一边的,volatile 变量仅能被安全地用在单一的载入或存储操作。这个限制导致 volatile 变量的使用是不常见的。 J2SE 5.0 提供了一组 atomic class 来处理更复杂的情况。相对于只能够做单一的 atomic 操作(如载入或存储),这 些 class能够让多个操作被 atomic地对待。这听起来像是不太 重要的加强版,但是一个简单的 atomic“比对后设定(compare-and-set)”操作就能够 让 thread 去“撷取标记”。反过来说,这让它能实现出 locking 的机制,事实上, ReentrantLock只使用atomic的 class就做出很多这样的功能。理论上来说是有可能不 靠 Java 的同步机制就实现出我们目前所办到的一切。 在这一节中,我们会来查看这些 atomic class。这 些 atomic的 class有两个用途。首先是 比较简单的,它提供class对单一的数据片段执行atomic操作。举例来说,有一个volatile integer无法使用++运算符是因为++运算符带有多个指令。然而AtomicInteger class 带有一个可以让它持有的 integer 能够 atomic 地递增(还是没有用到同步)。 其次,更复杂的,使用 atomic class 构造出完全不需要同步的复杂程序代码。需要访问 两个或两个以上 atomic 变量的程序代码(或者是对单一的 atomic 变量执行两个或两个 以上的操作)通常都需要被 synchronize以便两者的操作能够被当作是一个 atomic的单 元。无论如何,通过使用与 atomic class 所使用的相同方式的程序技巧,你也可以避开 同步来设计算法以执行这些多重的运算操作。 Atomic Class 的概述 由 AtomicInteger、AtomicLong、AtomicBoolean 与AtomicReference 这四个class 实现的四个基本的atomic类型来处理 integer、long、boolean 与对象。这些 class都提供 两个 constructor。默认的 constructor的值为零、false、false或 null,依数据类型来初始 化对象。另一个 constructor 是以程序设计师所指定的值来初始与创建变量。set()与 get()方法提供 volatile 变量所具有的功能:能够 atomic 地设定与取得值。get()与 set()方法也能够确保数据的读写是从主存储器上运行的。 87 93极简同步技巧 这些 class 的 getAndSet()方法提供新的功能。这个 method 能够在返回初始值的时候 atomic化地设定变量成新的值,完全不需要任何的同步 lock。要知道,只在 Java层使用 get与 set的运算符而没有使用同步是不可能模拟出这样的atomic化功能。如果这是不可 能的,那它又是怎么实现出来的?这个功能的实现是通过使用 user-level的 Java程序无 法访问的固有method 来完成的。你也可以自行编写固有的 method来完成此功能,但是 特定平台的问题是相当地令人生畏。此外,因为 atomic的 class是 Java的核心 class,它 们并不会有关于用户自定义固有 method 的安全性问题。 compareAndSet()与 weakCompareAndSet()是有条件的修改程序的方法。这两个 method 都要取用两个参数 —— 在 method 启动时预期数据所具有的值,以及要把数据 所设定成的值。method只会在变量具有预期值的时候才会将它设定成新值。如果当前值 不等于预期值,该变量不会被重新赋值且 method 返回 false。如果当前值等于预期值会 返回 boolean 的 true值,在这种情况下,值会被设定成新值。这个 method 的 weak 形式 基本上也是一样,但是少了一项保证:如果 method返回的是 false 值,该变量不会被变 动,但是这并不表示现有值不是预期值。这个 method 不管初始值是否为预期值都可能 会无法更新该值。 AtomicInteger 与AtomicLong 两个 class提供额外的 method 来支持 integer与 long 数 据类型。有意思的是这些便利性的 method 在内部是使用“比对后设定”功能来实现的。 无论如何,这些 method 是重要且常用的。 incrementAndGet()、decrementAndGet()、getAndIncrement()与getAndDecrement() 方法提供了前置递增( pre-increment)、前置递减、后递增( post-increment)与后递减 的功能。它们之所以必需是因为Java的递增与递减运算符都是多重载入与存储操作的语 义缩写(syntactic sugar),这些操作在volatile变量上并不是atomic的。使用atomic class 能够让你 atomic 地处理这些操作。 addAndGet()与 getAndAdd()方法提供“前置”与“后”的运算符给指定值( delta 值) 的加法运算。这些 method 让程序能够对变量增或减一个指定值,包括了负值,所以不 需要一个相对的减法运算 method。 atomic package 支持更复杂的变量类型吗?可以说支持也可以说不支持。目前是没有 实现atomic字符或浮点变量。你可以使用AtomicInteger来处理字符,但是使用atomic 的浮点数需要atomic带有只读浮点数值的受管理对象。我们会在本章稍后查看此情形。 有一些支持array 与变量的class 已经包含在其他的对象中。然而,没有额外的功能是由 这些 class所提供的,所以对复杂类型的支持是很少的。对 array 来说,一次只有一个索 引变量可以变动,并没有功能可以对整个 array做 atomic化的变动。atomic的 array是使 88 第五章94 用AtomicIntegerArray、AtomicLongArray与AtomicReferenceArray这些class来 模型化。这些 class的行为如同其所组成的数据类型的array,但是 array的大小必须在构 造时指定好,且在操作过程中必须提供索引。并没有 class 实现 boolean 的 array。这只 是小小的不方便,因为这样的 array 可以用 AtomicIntegerArray 来模拟。 最后由两个 class 来完成我们对 atomic class 的概述。AtomicMarkableReference 与 AtomicStampedReference能够让mark或stamp跟在任何对象的引用上。更精确地说, AtomicMarkableReference 提供一种包括对象引用结合 boolean 的数据结构,而 AtomicStampedReference 提供一种包括对象引用结合 integer 的数据结构。 这些 class的基本 method在本质上是相同的,只有稍稍地修改过以容许两个值(引用以 及 stamp 或 mark)。get()方法现在需要传入一个 array 作为参数,stamp 或 mark 被存 储在 array 的第一个元素而引用被正常地返回。其他的 get method就是返回引用、mark 或 stamp。set()与 compareAndSet()方法需要额外的参数来代表 mark 或 stamp。最 后,这些 class 带有 attemptMark()或 attemptStamp()方法,用来依据期待的引用设 定 mark 或 stamp。 使用 Atomic Class 如同我们前面所提的,是有可能(理论上) 将每个程序或class到目前为止已经实现出来 的部分改成只用 atomic 变量。老实说,不是那么简单。 atomic class 并不是同步工具的 直接替代品,使用它们需要对程序做更复杂的重新设计,就算是在某些简单的class中也 是一样。为了能够有更好的了解,让我们将 ScoreLabel 这个 class(注 2)修改成只使 用 atomic 变量: package javathreads.examples.ch05.example1; import javax.swing.*; import java.awt.event.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import javathreads.examples.ch05.*; public class ScoreLabel extends JLabel implements CharacterListener { private AtomicInteger score = new AtomicInteger(0); private AtomicInteger char2type = new AtomicInteger(-1); 注 2: ScoreLabel 同时也是我们第一个使用 J2SE 5.0 的 generic 功能的范例。你会看到在角 括号中的parameterized 程序代码。在这个 class 中, 是个 generic 引用。更进一步的细节可以在 David Flanagan 与 Brett McLaughlin 合著的《 Java 5.0 Tiger:程序高手秘笈》中看到(奥莱理,中文版已经发行) 。 89 95极简同步技巧 private AtomicReference generator = null; private AtomicReference typist = null; public ScoreLabel (CharacterSource generator, CharacterSource typist) { this.generator = new AtomicReference(generator); this.typist = new AtomicReference(typist); if (generator != null) generator.addCharacterListener(this); if (typist != null) typist.addCharacterListener(this); } public ScoreLabel () { this(null, null); } public void resetGenerator(CharacterSource newGenerator) { CharacterSource oldGenerator; if (newGenerator != null) newGenerator.addCharacterListener(this); oldGenerator = generator.getAndSet(newGenerator); if (oldGenerator != null) oldGenerator.removeCharacterListener(this); } public void resetTypist(CharacterSource newTypist) { CharacterSource oldTypist; if (newTypist != null) newTypist.addCharacterListener(this); oldTypist = typist.getAndSet(newTypist); if (oldTypist != null) oldTypist.removeCharacterListener(this); } public void resetScore() { score.set(0); char2type.set(-1); setScore(); } private void setScore() { // 此 method 稍后会在第七章解释 SwingUtilities.invokeLater(new Runnable() { public void run() { setText(Integer.toString(score.get())); } }); } 90 第五章96 public void newCharacter(CharacterEvent ce) { int oldChar2type; // 前一个字母没有正确输入:扣一分 if (ce.source == generator.get()) { oldChar2type = char2type.getAndSet(ce.character); if (oldChar2type != -1) { score.decrementAndGet(); setScore(); } } // 如果字母是无关的:扣一分 // 如果字母不相符:扣一分 else if (ce.source == typist.get()) { while (true) { oldChar2type = char2type.get(); if (oldChar2type != ce.character) { score.decrementAndGet(); break; } else if (char2type.compareAndSet(oldChar2type, -1)) { score.incrementAndGet(); break; } } setScore(); } } } 当你将这个class与之前的作比较时,就会发现变动不只是将前面由同步所保护的变量换 成atomic变量。移除掉同步也会对算法产生不同方面的影响。我们在此做出了三种修改: 简单的变量代换、算法的变更与重新尝试操作。 每一种修改的要点都在于保持class的 synchronized版本语义的完整。synchronized程序 代码的语义依赖于实现程序代码所有的效果。确保变量是由程序代码以atomic地更新来 运用是不够的,还必须确保程序代码的最终效果与 synchronized版本是一致的。我们接 下来会讨论到不同种类的修改以观察上述需求的含意。 变量代换 你可能做出最简单的修改是就将之前的 synchronized method 所用的变量替换成 atomic 变量。这就是我们在 resetScroe()方法中所做的事情:score与char2type变量已经 被改成 atomic 变量,且 method 只有把它们重新初始化。 很有意思的是,将此两个变量一起变更的动作并不是 atomic 地完成:还是有可能会让 char2type 变量的变更在完成前就变更了 score。这听起来会是一个问题,但实际上不 91 97极简同步技巧 是,因为我们还是保持住这个 class 在 synchronized 版本上的语义。之前对 ScoreLabel 这个 class 的实现有着类似的 race condition,那有可能导致如果在 resetScore()方法 被调用时监听者还跟在资源上而发生分数没有更新的情况。 在之前的实现中,resetScore()与 newCharacter()方法都是 synchronized,但是这 只意味着两者不会同时地运行,被拖延住的newCharacter()方法的调用还是可能会因 为到达的顺序或者取得 lock 的顺序而延迟运行(以 resetScore()的观点来说)。所 以打字输入的事件可能会等到resetScore()方法完成后才会被传递,但届时传递到的 只是个已经过时的事件。这与此次的实现有着相同的问题,就是在 resetScore()方 法中同时变更两个变量这个动作并没有被 atomic 地处理。 要记得同步的目的不是要防止所有的 race condition,它要防的是有问题的 race condition。此次关于resetScore()方法的实现所出现的race condition并不被认为是个 问题。稍后在本章的任一例子中我们都会创建此打字游戏的 atomic 变更分数与字母版 本。 变更算法 第二种变更是发生在resetGenerator()与resetTypist()这两个method的新的实现 中。之前对 resetGenerator()与 resetTypist()所做的,尝试将两者的同步 lock 分 离是个不错的主意。这两个方法都没有变动 score或者 char2Type变量,事实上,它 们甚至也没有变动到相互共享的变量 —— resetGenerator()方法的同步 lock 只是用 来保护此method不受多个thread同时地调用。resetTypist()方法也是这样。事实上, 这两个method的问题都一样,所以我们只讨论 resetGenerator()方法。很不幸地, 将 generator 变量变成 AtomicReference 会引发多个我们曾经解决过的潜在问题。 问题会发生是因为由resetGenerator()方法所封装的状态不只是generator变量的值。 让 generator 变量变成 AtomicReference 表示我们知道对该变量的操作是 atomic 地发 生。但当我们要从 resetGenerator()方法中完全地删除掉同步的时候,我们还必须 确保整个由此 method 所封装住的状态还是一致的。 在这个例子中,这个状态包括了在字母来源产生器之上ScoreLabel对象的登记(this 对象)。在这个 method 完成后,我们要确保 this 对象只有被登记过一次到唯一一个产 生器上(被分配到 generator 的 instance 变量上的那一个)。 思考一下当两个 thread 同时调用 resetGenerator()方法会发生什么事情。在此讨论 中,现有的产生器是 generatorA;某一个 thread 以 generatorB 产生器调用 resetGenerator()方法;而另一个 thread 以称为 generatorC 的产生器来调用此 method。 92 第五章98 前一个范例看起来像是下面这样: if (generator != null) generator.removeCharacterListener(this); generator = newGenerator; if (newGenerator != null) newGenerator.addCharacterListener(this); 在这个程序代码中,两个 thread同时要求generatorA删除this对象:实际上它会被 删除两次。ScoreLabel对象同样也会加入generatorB与generatorC。这两个结果都 是错的。 因为前一个范例是 synchronized,会防止这样的错误发生。在我们没有 synchronized 程 序代码中,则必须这样做: if (newGenerator != null) newGenerator.addCharacterListener(this); oldGenerator = generator.getAndSet(newGenerator); if (oldGenerator != null) oldGenerator.removeCharacterListener(this); 这个程序代码的效果必须仔细考虑。当它被两个 thread同时调用时,ScoreLabel对象 会被 generatorB 与 generatorC 登记。各 thread 随后会 atomic 地设定当前的产生器。 因为它们是同时运行,可能会有不同的结果。假设第一个 thread 先运行:它会从 getAndSet()方法中取回generatorA 然后将ScoreLabel对象从generatorA的 监听器中删除。第二个 thread 从 getAndSet()方法中取回 generatorB 并从 generatorB 的监听器删除 ScoreLabel。如果第二个 thread先运行,变量会稍有不 同,但结果永远会是一样的:不管哪一个对象被分配给 generator 的 instance 变量, 它就是 ScoreLabel 对象所监听的那一个(唯一的一个) 。 此处会有一个副作用影响到其他method。因为在交换之后监听器会从旧的数据来源中被 删除掉,且监听器会在交换前被加入到新的数据来源,它现在有可能接收到既不是现有 的产生器也不是打字输入来源所产出的字符。之前 newCharacter()方法会检查来源 是否为产生器的来源,并在不是的时候会假设来源是打字输入来源。现在就不再是这样 了,newCharacter()方法现在必须在处理它之前确认字母的来源,它必须忽略掉来自 不正确的监听器的字母。 重新尝试操作 在此范例中newCharacter()方法的改变最多。如同之前所提过的,第一个修改是将事 件根据不同的字母来源分离开来。这个 method 现在不能假设当来源不是产生器的时候 就会是打字输入:它必须要丢弃任何不是来自于所属来源的事件。 93 99极简同步技巧 对产生器事件的处理只有小幅度的修改。首先,getAndSet()方法是用来atomic地用新 值交换字母。其次,玩家直到交换之后才会被扣分。这是因为没有办法可以确保之前的 字母是什么,直到getAndSet()方法的交换完成后。此外,分数也必须atomic 地递减, 因为它可能会被多个同时到达的事件变更过。对于字母与分数的变更并不是被atomic地 处理掉:还是存有 race condition。然而,它也不会是问题。我们需要正确地更新分数 以奖励或处罚玩家。如果玩家在分数更新前遇到非常短暂的延迟并不会是个问题。 对打字输入事件的处理则更为复杂。我们需要检查输入的字母是否是正确的。如果不是, 玩家就会被处罚。这是通过atomic地递减分数来完成的。如果字母被正确地输入,玩家 无法被立即地给予奖赏。相对地,char2type变量要先被更新,分数只有在char2type 被正确更新时才会更新。如果更新的操作失败了,这代表着在我们处理此事件时有其他 的事件已经被处理过(在其他 thread 中)且其他的操作是成功处理完的。 其他操作成功地处理完另一个事件是什么意思?它表示我们必须重新处理事件。我们是 做出如此的假设:假设正在使用的该变量值不会被变更且程序代码完成时也是如此,所 有已经被我们设定为具有特定值的变量就确实应该是那个值。但因为这与其他thread冲 突到,这些假设就被破坏了。通过重新尝试处理事件,就好像从未遇到冲突一样。 那也就是为何这一段程序代码被封装在一个无穷循环中的原因:程序不会离开循环直到 事件被成功地处理掉。显然在多个事件间有着 race condition,循环确保没有一个事件 会被漏掉或者处理了超过一次。只要我们确实只处理有效的事件一次,事件被处理的顺 序就不重要了:在处理完每个事件后,数据就会保持在完好的状态。注意,当我们使用 同步的时候,也有同样的情形:多个事件并没有以指定的顺序进行,它们只是根据授予 lock 的顺序来进行。 atomic变量的目的只是避免同步的性能因素。然而,atomic变量又是如何能够在置于无 穷循环中的时候还比较快呢?当然,从技术上来说答案是那并不是个无穷的循环。额外 的重复循环只会发生在atomic操作失败的时候,这是因为与其他的thread发生冲突。要 发生一个真正的无穷循环,那会需要无穷的冲突。如果使用同步,这也会是个问题:无 数的thread要访问lock同样也会让程序无法正常地操作。另外一方面,第十四章会讨论 到 atomic class 与同步间的性能差异通常不是很大,所以没有必要特别注意。 如同我们在此范例中可以发现到的,有必要平衡同步与atomic变量的使用。在使用同步 的时候,thread 在取得 lock 之前会被 block 住而不能执行。这能够让程序因为其他的 thread被阻挡不能执行而atomic地来运行。当使用 atomic变量时,thread是能够并行地 运行相同的程序代码。atomic 变量的目的不是消除不具有 thread 安全性的 race condition,它们的目的是要让程序代码具有thread安全性,所以就不用特地去防止 race condition。 94 第五章100 通知与 Atomic 变量 是否在需要条件变量的功能时也能够使用atomic 变量?使用atomic变量来实现条件变 量的功能是有可能的,但必不一定有效率。同步以及等待与通知机制是以控制 thread的 状态来实现的。如果 thread 无法取得lock 就会被 block 住不能执行,且会被设定为等待 的状态直到特定的条件发生为止。atomic 变量并不会 block 住 thread 的执行,事实上, 由非synchronized thread所执行的程序代码可能会被设定到更复杂的循环中以便能够重 新尝试失败的动作。换言之,是有可能以atomic变量来实现出条件变量的功能,但thread 会在等待所需的条件的同时不停地来回执行着。 这并不代表如果需要条件变量的功能就应该避免atomic变量。再一次说明, 还是要取得 使用的平衡。有可能在程序中不一定要运用通知与使用同步的部分来使用 atomic变量, 也有可能将整个程序实现以atomic变量以及用独立的函数库来送出通知,在内部使用条 件变量的函数库。当然,在某些情况下让thread在等待的时候来回执行也不会是个问题。 最后一个方案正是打字游戏所处的情况。首先,只有两个 thread —— 动画组件的thread 与字母产生器的thread,需要等待条件。第二,等待的处理只会在游戏停止的时候发生。 程序已经在动画的两个frame之间等待,使用相同的循环与间隔来等待玩家重新启动游 戏并不会对性能产生重大影响。第三, 在 Start按钮按下时等待100个毫秒(动画的frame 间隔周期)应该不会让玩家有感觉,任何注意到此延迟的玩家应该也同时会注意到动画 本身的延迟。 下面是只使用atomic 变量来实现动画组件,它会在使用者停止游戏的时候来回地运行。 随机字母产生器的类似的实现可以从在线范例取得。 package javathreads.examples.ch05.example2; import java.awt.*; import javax.swing.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import javathreads.examples.ch05.*; public class AnimatedCharacterDisplayCanvas extends CharacterDisplayCanvas implements CharacterListener, Runnable { private AtomicBoolean done = new AtomicBoolean(true); private AtomicInteger curX = new AtomicInteger(0); private AtomicInteger tempChar = new AtomicInteger(0); private Thread timer = null; public AnimatedCharacterDisplayCanvas() { startAnimationThread(); } 95 101极简同步技巧 public AnimatedCharacterDisplayCanvas(CharacterSource cs) { super(cs); startAnimationThread(); } private void startAnimationThread() { if (timer == null) { timer = new Thread(this); timer.start(); } } public void newCharacter(CharacterEvent ce) { curX.set(0); tempChar.set(ce.character); repaint(); } protected void paintComponent(Graphics gc) { char[] localTmpChar = new char[1]; localTmpChar[0] = (char) tempChar.get(); int localCurX = curX.get(); Dimension d = getSize(); int charWidth = fm.charWidth(localTmpChar[0]); gc.clearRect(0, 0, d.width, d.height); if (localTmpChar[0] == 0) return; gc.drawChars(localTmpChar, 0, 1, localCurX, fontHeight); curX.getAndIncrement(); } public void run() { while (true) { try { Thread.sleep(100); if (!done.get()) { repaint(); } } catch (InterruptedException ie) { return; } } } public void setDone(boolean b) { done.set(b); } } 如同前一个范例,使用 atomic变量不只是将由同步保护的变量替换成atomic变量而已, 算法同样也需要以能够让任何 race condition 具有 thread 安全性的形式来调整。在动画 97 第五章102 组件中,创建动画 thread的程序代码更需要这样做。在前一个范例中,当 setDone()方 法被调用时就会创建这个 thread。我们可以不改变该 method 的程序代码并使用 atomic 引用变量来存储此thread对象,只有成功存储 atomic引用的thread才会去调用新thread 的启动method。然而,将此功能在只有该对象的 constructor 才会调用的private method 中创建与启动 thread 是比较容易的(因为 constructor 不会被多个 thread 调用到)。 newCharacter()方法只是部分的atomic。个别变量的操作,如 curX与tempChar的赋 值,都因为使用到 atomic 变量而是 atomic 的。然而,这两个赋值动作一起发生的时候 就不是 atomic 的。如果其他的 thread 同时调用 newCharacter()方法也不会是问题。 两个method的调用会设定curX变量为零,且字母变量也会被赋值为由第二个thread执 行该 method 时请求的字母。在此 method 与 paintComponent()方法间还是有 race condition,但或许没有被注意到。这个 race condition 会让 paintComponent()方法产 生假递增。这意味着新的字母会从第二个动画 frame开始画起,第一个动画 frame会被 跳过 —— 一个不太可能会被玩家注意到的效果。 paintComponent()方法同样也不是完全地 atomic,但也跟 newCharacter()方法一 样,所有的 race condition 都是可接受的。它是不可能跟自己产生冲突的,因为 paintComponent()方法只会被窗口系统所调用且只会来自单一的thread。所以,没有 理由要保护只被paintComponent()方法用到的变量。paintComponent()方法会 加载与 newCharacter()方法共享的数据到临时的变量中。如果这些变量刚好在 paintComponent()方法调用的时候被变更,这不会是个问题,因为另外一个 repaint()方法的请求也会由newCharacter()方法所发出。这样的结果最多也只不 过是个假的 frame。 run()这个 method 类似我们之前的版本,它会在 done 标记是 false 的时候每隔 100毫 秒就调用 repaint()方法。然而,如果 done标记被设定为 true,此 thread还是会每隔 100毫秒醒来一次。这意味着这个程序每隔 100 毫秒就执行一次“什么都不做”的 task。 这个 thread 在动画运行的期间每隔 100 毫秒就执行一次,即使是游戏停止也还是这样。 在另外一方面,动画的复原就不再是立即地运行:使用者最多会等待 100毫秒才会看到 动画重新启动。这可以通过从 setDone()方法调用 repaint()方法来解决,但这个范 例并不需要这么做。动画的 frame间的延迟是 100毫秒。如果启动动画的 100毫秒延迟 会被注意到,那么 frame 间的 100 毫秒延迟也就同样地会被注意到。 现在setDone()方法的实现简单多了。它不再需要创建动画的 thread,因为它现在是在 组件的构造期间所完成的,且它也不需要通知动画的 thread关于done标记被变动过的 事情。 98 103极简同步技巧 这个实现主要的好处在于此组件中不再有任何的同步。在游戏没有进行的时候会有些 threading 的消耗,但它还是比游戏进行时的消耗要小。其他的程序可能会有不同的背 景,如同我们之前所提过的,开发者不只面对使用同步技巧还是atomic变量的选择而已, 它们还需要在两者之间取得平衡。为了要了解这个平衡,最好能在实例中将两者都使用 到。 使用 Atomic 变量的总结 这些范例展示出数种 atomic 变量的规范使用方式,我们也用了许多的技巧来扩展由 atomic 变量所提供的 atomic 操作。以下是这些技巧的总结。 乐观同步 在使用 atomic变量的范例中所发生的事情是所谓的“没有白吃的午餐” :此程序代 码避开了同步,但它在所运作的工作量上却付出了代价。你可以把这想做是个 “乐 观同步”(此词修改自数据库管理的术语):程序代码抓住保护变量的值并作出此一 瞬间没有其他修改的假设,然后程序代码就计算出该变量的新值并尝试更新该变 量。如果有其他的 thread同时修改了变量,这个更新就失败且程序必须重新执行这 些步骤(使用变量的最新修改过的值)。 这个atomic class在实现内部使用这个技巧,且我们也在范例中对 atomic变量有多 个操作时使用这个技巧。 数据交换 数据交换是在取得旧值的同时atomic地设定新值的能力。这是使用getAndSet()方法 来完成的。使用这个 method 能够确保只有一个 thread 能够取得并使用该值。 如果有更复杂的数据交换时该如何处理?如果值的设定要依据旧值又该如何处理?这可 以通过把 get()与 compareAndSet()方法放在循环中来处理。get()方法用来取得旧 值,以计算新值。此变量以使用 compareAndSet()方法来设定新值 —— 只有在旧值 没有被更动的时候才会设定为新值。如果 compareAndSet()方法失败,整个操作可以 重新进行,因为当前的 thread 在失败时都没有动到任何数据。虽然调用过 get()方法、 计算过新值,数据的交换并不是被个别地 atomic,如果它成功,这个顺序可以被认为是 atomic 的,因为它只在没有其他 thread 变动该值时才会成功。 99 第五章104 比较与设定 比较与设定是只有在当前值是预期值的时候才atomic地设定值的能力。compareAndSet() 这个 method 就是用来处理这个状况,这是在 atomic 层提供条件式支持能力的重要 method。这个基本的功能甚至能够用来实现出由 mutex 所提供的同步能力。 如果更复杂的比较该如何处理?如果比较是要依据旧值或外部值该如何处理?跟前面一 样,这个范例可通过把 get()与 compareAndSet()方法放在循环中来处理。get()方 法可以用来取得旧值,以用来比较或者只用来完成 atomic的交换。复杂的比较可以用来 观察是否要继续操作,然后使用 compareAndSet()方法在当前值没有被更动的情况下 设定新值。如果操作失败整个操作就会重新进行。同前,如果它成功,这个顺序可以被 认为是 atomic 的,因为它只在符合操作开始时的值才会被设定。 高级的 atomic 数据类型 虽然atomic class可用的数据类型列表数量是相当的大,但不是完整的。 atomic package 并没有支持字符与浮点数类型。虽然它支持一般对象类型,但并没有对更复杂的对象类 型提供支持,像是 string 所需的操作。然而,我们可以将数据类型封装进只读的数据对 象中来对任何的新类型实现 atomic 的支持。然后此数据对象通过改变 atomic 引用至新 的数据对象,就可以被atomic地变动。这仅在嵌入数据对象的值不会被任何方式变动时 才有效。任何对数据对象的变动必须只能通过改变引用到不同的对象来完成 —— 旧对 象的值是不变的。所有由数据对象所封装的值,不管是直接还是非直接的,必须是只读 的才能使这个技巧有效地运作。 因此,不可能 atomic 地改变浮点数值,但却有可能 atomic 地改变对象引用到不同的浮 点数值。只要浮点数的值是只读的,这个技巧就会是具有 thread安全性的。记住上面的 方法,我们就可以实现出浮点数值的 atomic class: package javathreads.examples.ch05; import java.lang.*; import java.util.concurrent.atomic.*; public class AtomicDouble extends Number { private AtomicReference value; public AtomicDouble() { this(0.0); } public AtomicDouble(double initVal) { value = new AtomicReference(new Double(initVal)); } 100 105极简同步技巧 public double get() { return value.get().doubleValue(); } public void set(double newVal) { value.set(new Double(newVal)); } public boolean compareAndSet(double expect, double update) { Double origVal, newVal; newVal = new Double(update); while (true) { origVal = value.get(); if (Double.compare(origVal.doubleValue(), expect) == 0) { if (value.compareAndSet(origVal, newVal)) return true; } else { return false; } } } public boolean weakCompareAndSet(double expect, double update) { return compareAndSet(expect, update); } public double getAndSet(double setVal) { Double origVal, newVal; newVal = new Double(setVal); while (true) { origVal = value.get(); if (value.compareAndSet(origVal, newVal)) return origVal.doubleValue(); } } public double getAndAdd(double delta) { Double origVal, newVal; while (true) { origVal = value.get(); newVal = new Double(origVal.doubleValue() + delta); if (value.compareAndSet(origVal, newVal)) return origVal.doubleValue(); } } public double addAndGet(double delta) { Double origVal, newVal; while (true) { origVal = value.get(); newVal = new Double(origVal.doubleValue() + delta); 101 第五章106 if (value.compareAndSet(origVal, newVal)) return newVal.doubleValue(); } } public double getAndIncrement() { return getAndAdd((double) 1.0); } public double getAndDecrement() { return getAndAdd((double) -1.0); } public double incrementAndGet() { return addAndGet((double) 1.0); } public double decrementAndGet() { return addAndGet((double) -1.0); } public double getAndMultiply(double multiple) { Double origVal, newVal; while (true) { origVal = value.get(); newVal = new Double(origVal.doubleValue() * multiple); if (value.compareAndSet(origVal, newVal)) return origVal.doubleValue(); } } public double multiplyAndGet(double multiple) { Double origVal, newVal; while (true) { origVal = value.get(); newVal = new Double(origVal.doubleValue() * multiple); if (value.compareAndSet(origVal, newVal)) return newVal.doubleValue(); } } } 在新的 AtomicDouble这个 class中,我们使用了 atomic引用对象来封装一个 double的 浮点数值。因为 Double class 已经封装一个 double 值,所以就不需要创建新的 class, 此 Double class 用来维护该 double 值。 get()现在必须使用两个对method的调用来取得double值 —— 它必须要取得Double 对象,那是用来取得 double 浮点数值。取得 Double 对象类型显然是 atomic 的,因为 我们是使用atomic引用对象来持有该对象。无论如何,整体技巧能够运作都赖于数据是 102 107极简同步技巧 只读的:它不能被更改。如果数据不是只读的,读出数据就不会是 atomic的,且两个并 用的 method 也不会被视为是 atomic 的。 set()方法是用来改变值的。因为被封装的值是只读的,我们必须创建新的 Double对 象而不是改变旧值。如同 atomic 引用本身的操作一样,这会是 atomic 的,因为我们是 运用 atomic 引用对象来改变引用的值。 compareAndSet()方法是以之前提过的复杂比对后设定技巧来实现的,getAndSet() 方法是以之前提过的复杂数据交换技巧来实现的。而其他的 method,如加法、乘法等, 也是以复杂的数据交换技巧来实现。我们没有于本章直接地展示出这个class的范例,但 会于第十五章中用到它。现在这个 class是个相当好的framework,能够对新的与复杂的 数据类型实现出 atomic 支持。 大量数据的更改 在之前的范例中,我们只有对个别的变量做 atomic 地设定,还没有做到对一群数据 atomic地设定。在那些对一个以上的变量做设定的例子中,我们并没有考虑到要把它们 当作一组来做atomic地设定。无论如何,atomic地设定一组变量可以由创建封装这些要 被变动值的对象来完成,之后这些值就可以通过 atomic 地变动对这些值的 atomic 引用 来做到同时的改变。这样的运行方式完全就和 AtomicDouble 这个 class 一样。 再一次的说明,这只有在值没有以任何方式直接改变的情况下才会有效。任何对数据对 象的变更是通过改变引用到不同的对象上来完成的 —— 旧的对象值必须没有被变动过。 不管是直接还是间接封装的值都必须是只读的才能让这个技巧有效运作。 以下是个用来保护两个变量的 atomic class:分数与字母变量。我们可以使用这个 class 来 atomic 地开发修改分数与字母变量的打字游戏: package javathreads.examples.ch05.example3; import java.util.concurrent.atomic.*; public class AtomicScoreAndCharacter { public class ScoreAndCharacter { private int score, char2type; public ScoreAndCharacter(int score, int char2type) { this.score = score; this.char2type = char2type; } public int getScore() { return score; } 103 第五章108 public int getCharacter() { return char2type; } } private AtomicReference value; public AtomicScoreAndCharacter() { this(0, -1); } public AtomicScoreAndCharacter(int initScore, int initChar) { value = new AtomicReference (new ScoreAndCharacter(initScore, initChar)); } public int getScore() { return value.get().getScore(); } public int getCharacter() { return value.get().getCharacter(); } public void set(int newScore, int newChar) { value.set(new ScoreAndCharacter(newScore, newChar)); } public void setScore(int newScore) { ScoreAndCharacter origVal, newVal; while (true) { origVal = value.get(); newVal = new ScoreAndCharacter (newScore, origVal.getCharacter()); if (value.compareAndSet(origVal, newVal)) break; } } public void setCharacter(int newCharacter) { ScoreAndCharacter origVal, newVal; while (true) { origVal = value.get(); newVal = new ScoreAndCharacter (origVal.getScore(), newCharacter); if (value.compareAndSet(origVal, newVal)) break; } } public void setCharacterUpdateScore(int newCharacter) { ScoreAndCharacter origVal, newVal; int score; 104 109极简同步技巧 while (true) { origVal = value.get(); score = origVal.getScore(); score = (origVal.getCharacter() == -1) ? score : score-1; newVal = new ScoreAndCharacter (score, newCharacter); if (value.compareAndSet(origVal, newVal)) break; } } public boolean processCharacter(int typedChar) { ScoreAndCharacter origVal, newVal; int origScore, origCharacter; boolean retValue; while (true) { origVal = value.get(); origScore = origVal.getScore(); origCharacter = origVal.getCharacter(); if (typedChar == origCharacter) { origCharacter = -1; origScore++; retValue = true; } else { origScore--; retValue = false; } newVal = new ScoreAndCharacter(origScore, origCharacter); if (value.compareAndSet(origVal, newVal)) break; } return retValue; } } 如同在AtomicDouble这个class中一样,getScore()与getCharacter()方法能运行 是因为被封装的值是以只读的方式处理。set()方法必须创建新的对象来封装被保存的 新值。 setScore()与 setCharacter()方法是使用高等数据交换计数来实现出的。这是因为 此实现技术是交换数据而不只是设定数据。虽然只有改变一部分的封装数据,我们还是 得读取没有被改变的数据(用来确保它实际上没有被变更) 。且因为我们必须对整组的 数据做 atomic地修改 —— 保证让不想被修改的数据不会被更动,所以必须将程序代码 实现成数据交换。 setCharacterUpdateScore()与processCharacter()方法是实现此计分系统的核心。 第一个method设定要被输入的字母,并在玩家没有正确地输入前一个字母时做出处罚。 第二个 method 比对输入的字母与当前产生出的字母。如果两者相符,此字母被设定成 非字母的值,且分数会递增;如果两者并不相符,就直接扣分。有意思的是虽然这两个 105 第五章110 method是如此的复杂,但它们还是 atomic的,因为所有的计算都是在临时变量上完成, 且所有的值都是使用数据交换来做 atomic 地变更。 以高级atomic数据类型来执行大量数据变更,可能会用到大量的对象。对每个事务动作 都需要创建一个新的对象,而不管有多少的变量需要修改。每个 atomic的比对和设定操 作在失败而必须重新尝试的时候也需要创建新的对象。再说一次, atomic变量的使用必 须与使用同步之间取得平衡。是否可以接受所有临时对象的创建?此技巧是否比同步 好?或者说这是个折中方案?答案与特定程序有关。 如同这些技巧所展示出来的,使用 atomic变量是有点复杂。这些复杂性发生在使用多个 atomic 变量时、对单一 atomic 变量的多重操作或在一个必须是 atomic 的程序段中两者 兼备。在许多情况中,因为只是想要用 atomic变量来做单一的操作,例如更新分数,它 们是很容易使用的。 而在其他的情况下,使用这一类极简同步并不是个好主意。它可能会变得很复杂,使得 程序代码难以维护或者让开发者间很难接手。对同步而言,大量 method 调用可能会是 问题,改以极简同步的好处还是有争议的。对于那些在class或子系统中相信问题是由同 步所引发的读者来说,重新审议这个主题是个好主意 —— 如果只是想要以极简同步来 获得较佳的满意度的话。 Thread 局部变量 任何的thread都可以在任意的时间定义该thread私有的局部变量。其他 thread可以定义 相同的变量以创建该变量自有的拷贝。这意味着 thread的局部变量无法用在thread间共 享状态,对某 thread 私有的变量所做的改变并不会反映在其他 thread 所持有的拷贝上。 但这也代表对该变量的访问绝不需要同步化,因为它不可能让多个 thread同时访问。当 然,thread 的局部变量还有其他的用途,但它们最常见的用途还是能够让多个 thread保 有自有的数据而不是对共享的数据来竞争同步 lock。 thread 的局部变量是以 java.lang.ThreadLocal 这个 class 来模型化: public class ThreadLocal { protected T initialValue(); public T get(); public void set(T value); public void remove(); } 在一般的使用中,会 subclass 此 ThreadLocal 并 override initialValue()方法来返 回应该在 thread 第一次访问此变量时返回的值。它的 subclass 很少需要 override 106 111极简同步技巧 ThreadLocal其他的method;相反地,这些method是用来作为特定thread值的getter/ setter 的 pattern。 有一个使用 thread 的局部变量以避免同步的例子是个别thread 使用的 cache。以下面的 class 来说: package javathreads.examples.ch05.example4; import java.util.*; public abstract class Calculator { private static ThreadLocal results = new ThreadLocal() { protected HashMap initialValue() { return new HashMap(); } }; public Object calculate(Object param) { HashMap hm = results.get(); Object o = hm.get(param); if (o != null) return o; o = doLocalCalculate(param); hm.put(param, o); return o; } protected abstract Object doLocalCalculate(Object param); } thread局部对象被声明成static,因此对象本身(也就是此例中的 results 变量)是在 thread间被共享。当 thread局部变量的 get()方法被调用时, thread 的 local class内部 机制会返回赋值给特定 thread 的特定对象。该对象的初始值是从 extend ThreadLocal 的 class 的 initialValue()方法返回。当你在创建 thread 局部变量时,你要负责实现 此 method 以返回适当(属于该 thread)的对象。 当范例中的 calculate()方法被调用时,会引用 thread 的私有 hash map 以判断值是否 之前就被计算过。如果是,该值就会被返回;否则,会执行计算且新值会存储于hash map 中。因为对此 map 的访问只会来自一个 thread,我们就能够使用 HashMap 对象而不是 Hashtable 对象(或者要同步化 has map)。 这个方式仅在因为取得 hash map 本身需要对所有的 thread 作同步化而使得计算非常昂 贵的时候才有意义。如果从 thread的 get()方法所返回的引用需要被长时间持有的话, 这种设计就值得多加探讨,不然此引用就需要被同步化很长一段时间;否则,你就只是 107 第五章112 以一个同步调用来取代另外一个罢了。一般来说,ThreadLocal的性能在过去是相当的 差,虽然这种情况在 JDK 1.4 中已经得到改善,且在 J2SE 5.0 中改变得更多。 此技巧也很有用的另外一种情况是处理不具thread安全性的class。如果每个thread都在 thread 局部变量中初始必要的对象,它就有一份可以安全访问的拷贝。 可继承的 Thread 局部变量 由 thread中的thread局部变量所存储的值之间是无关的。当一个新的 thread被创建的时 候,它会有一份新的 thread局部变量拷贝,且这些变量的值是由 thread 局部subclass 的 initialValue()方法所返回。 此想法的另一个变换是 InheritableThreadLocal 这个 class: package java.lang; public class InheritableThreadLocal extends ThreadLocal { protected Object childValue(Object parentValue); } 这个 class 能够让子 thread 继承父 thread 的 thread 局部变量值。也就是说,当 thread 局 部变量的 get()方法被子 thread所调用时,它所返回的值会与该 method 被父 thread调 用时所返回的一样。 如果想要的话,还可以使用 childValue()方法来更进一步地将此行为参数化。当子 thread 调用 thread 局部变量的 get()方法时,get()方法会查询父 thread 的值,然后 将该值传递给 childValue()方法并返回该值。默认地, childValue()方法只是返回 它的参数,所以不会发生转换。 总结 在本章中,我们查看过一些同步的高级技巧。学习到关于 Java内存模型以及为何它会妨 碍某些同步技巧预期般地运作。这会产生对 volatile 变量更好的认知以及了解到为何 Java 所推出的同步规则是如此的难以改变。 我们同样也查看过伴随J2SE 5.0而来的atomic package。这是一种可以避开同步的途径, 但为了它也是要付出代价的:在 atomic package 中的 class 有一种天性导致经常要为此 而必须改变算法(特别是在同时使用多个 atomic变量时)。创建一个直到所需的结果完 成时才会离开的循环是实现 atomic 变量时常见的方法。 108 113极简同步技巧 范例的 Class 下面列出本章范例的 class 名称与 Ant 的 target: 说明 主要的 Java class Ant target 使用 atomic ScoreLabel javathreads.examples.ch05.example1. ch5-ex1 的 Swing Type Tester SwingTypeTester 使用 atomic animation canvas javathreads.examples.ch05.example2. ch5-ex2 的 Swing Type Tester SwingTypeTester 使用 atomic score 与 javathreads.examples.ch05.example3. ch5-ex3 character class 的 SwingTypeTester Swing Type Tester 使用 thread 局部变量的 javathreads.examples.ch05.example4. ch5-ex4 计算测试 CalculatorTest 计算器测试需要命令行参数来设定同时运行的thread数目,在 Ant的指令中,它是以下 面这个属性来定义: 109
还剩27页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

zhengfeng

贡献于2012-03-17

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf