原始问题的简短答案
- 如果
Foo
是不变的,则只需将字段定为final将确保完全初始化,并且字段对所有线程的一致性可见性,而与同步无关。 - 是否
Foo
是不可变的,通过volatile theFoo
或进行发布,AtomicReference<Foo> theFoo
足以确保通过theFoo
引用读取的任何线程都可以看到对其字段的写入 - 使用对的简单分配
theFoo
, 永远不能 保证阅读器线程看到任何更新 - 在我看来,基于JCiP,是“落实记忆障碍最可读的方式”
AtomicReference<Foo>
,用明确的同步中第二次来,和使用的volatile
第三来临 - 可悲的是,我在斯卡拉没有任何提供
您可以使用 volatile
我怪你。现在我迷上了,我已经分解了JCiP,现在我想知道我编写的任何代码是否正确。上面的代码片段实际上可能是不一致的。(编辑:请参见通过挥发下方安全发布的部分。)读线程可以 同时 看到陈旧的(在这种情况下,无论默认值 您可以执行以下任一操作以引入“先发生边缘”:a
和b
均)为无限时间。
- 通过进行发布
volatile
,它会在monitorenter
(读面)或monitorexit
(写面)之前创建一个巧合边。 - 发布前使用
final
字段并在构造函数中初始化值 - 在将新值写入
theFoo
对象时引入同步块 - 使用
AtomicInteger
栏位
这些可以解决写顺序(并解决其可见性问题)。然后,您需要解决新
theFoo参考的可见性。在这里
volatile是合适的-JCiP在第3.1.4节“易变变量”中说(这里的
变量 是
theFoo):
仅当满足以下所有条件时,才能使用volatile变量:
- 写入变量不依赖于其当前值,或者您可以确保只有一个线程更新该值。
- 该变量不与其他状态变量一起参与不变式。和
- 由于其他原因,在访问变量时不需要锁定
如果您执行以下操作,那么您会很成功:
class Foo { // it turns out these fields may not be final, with the volatile publish, // the values will be seen under the new JMM final int a, b; Foo(final int a; final int b) { this.a = a; this.b=b; }}// without volatile here, separate threads A' calling readFoo()// may never see the new theFoo value, written by thread A static volatile Foo theFoo;void updateFoo(int newA, int newB) { f = new Foo(newA,newB); theFoo = f;}void readFoo() { final Foo f = theFoo; // use f...}简单明了
有关此主题和其他主题的一些人(感谢@JohnV)指出,有关这些问题的权威人士强调了 记录
同步行为和假设的重要性。JCiP对此进行了详细讨论,提供了一组可用于文档和静态检查的批注,并且您还可以查看JMM
Cookbook,了解有关特定行为的指标,这些指标需要文档和指向适当参考的链接。Doug
Lea还准备了记录并发行为时要考虑的问题列表。特别是由于对并发问题的关注,怀疑和困惑,所以文档是适当的(关于SO:“
Java并发玩世不恭吗?” )。另外,诸如FindBugs之类的工具现在也提供了静态检查规则,以注意违反JCiP注释语义的行为,例如“不一致的同步:IS_FIELD-
NOT_GUARDED”。
除非您认为自己有理由这样做,否则最好是使用
@Immutable和
@GuardedBy注释继续最易读的解决方案,例如(感谢@Burleigh
Bear)。
@Immutableclass Foo { final int a, b; Foo(final int a; final int b) { this.a = a; this.b=b; }}static final Object FooSync theFooSync = new Object();@GuardedBy("theFooSync");static Foo theFoo;void updateFoo(final int newA, final int newB) { f = new Foo(newA,newB); synchronized (theFooSync) {theFoo = f;}}void readFoo() { final Foo f; synchronized(theFooSync){f = theFoo;} // use f...}或者,可能是因为它更干净:
static AtomicReference<Foo> theFoo;void updateFoo(final int newA, final int newB) { theFoo.set(new Foo(newA,newB)); }void readFoo() { Foo f = theFoo.get(); ... }什么时候适合使用 volatile
实际上,谷歌搜索:“ site:stackoverflow.com + java + volatile+关键字”会返回355个不同的结果。使用
volatile充其量,挥发性决定。什么时候合适?JCiP提供了一些抽象指导(如上所示)。我将在此处收集一些更实用的准则:
- 我喜欢这个答案:“
volatile
可以用来安全地发布不可变的对象”,它巧妙地封装了应用程序程序员可能期望的大多数使用范围。 - @mdma 在这里的回答:“
volatile
在无锁算法中最有用”总结了另一类用途-专用无锁算法,其性能足以敏感地进行专家分析和验证。
通过挥发物安全发布
在@Jed Wesley-Smith之后),似乎
volatile现在提供了更强的保证(自JSR-133起),并且较早的断言“
volatile只要发布的对象是不变的,您就可以使用”就足够了,但也许没有必要。
查看JMM
FAQ,这两个条目在新的JMM下如何最终字段工作?和什么挥发性做?并没有真正一起处理,但我认为第二个方面满足了我们的需求:
区别在于,现在对它们周围的常规字段访问进行重新排序不再那么容易了。写入易失性字段具有与监视器释放相同的存储效果,而从易失性字段中读取具有与监视器获取相同的存储效果。实际上,由于新的内存模型对易失性字段访问与其他易失性字段访问的重新排序施加了更严格的约束,因此线程A写入易失性字段f时对线程A可见的任何内容在读取f时对线程B可见。
我会注意到,尽管对JCiP进行了多次重读,但直到Jed指出之前,那里的相关文本才跳出来。在第。38,第3.1.4节,它说的内容与前面的引用大致相同-
已发布的对象只需要有效地不可变,不需要
final字段,QED。
较旧的内容,用于问责制
一个评论:任何原因
newA而
newB不能参数构造函数?然后,您可以依靠构造函数的发布规则…
另外,使用
AtomicReference可能的方法可以消除所有不确定性(并可以根据课堂上其余课程的需要来给您带来其他好处……)此外,比我聪明的人可以告诉您是否
volatile可以解决这个问题,但是在我看来总是很神秘 …
在进一步的审查中,我认为上面@Burleigh Bear的注释是正确的—(编辑:请参见下文),您实际上不必担心此处的顺序乱序,因为您要发布新对象到theFoo
。而另一个线程可以想见,看到不一致的值newA
,并newB
在JLS17.11描述,因为其他线程获取阿霍德新的引用之前,他们将被提交到存储器中,可以发生在这里f = newFoo()
你已经创建的实例…这是安全的一次性出版物。另一方面,如果您写了
void updateFoo(int newA, int newB) { f = new Foo(); theFoo = f; f.a = newA; f.b = newB;}但是在那种情况下,同步问题是相当透明的,排序是您最少的担心。有关volatile的一些有用指南,请参阅这篇developerWorks文章。
但是,您可能会遇到一个问题,即单独的读取器线程可以在
theFoo无限制的时间内看到旧值。实际上,这种情况很少发生。但是,可以允许JVM
theFoo在另一个线程的上下文中缓存引用的值。我非常确定标记
theFoo为
volatile可以解决此问题,任何类型的同步器或都可以解决
AtomicReference。



