Java VM上的内存障碍和编码风格

假设我有一个静态的复杂对象,它被一个线程池定期更新,并且在一个长时间运行的线程中或多或less地持续读取。 对象本身总是不变的,反映了最近的状态。

class Foo() { int a, b; } static Foo theFoo; void updateFoo(int newA, int newB) { f = new Foo(); fa = newA; fb = newB; // HERE theFoo = f; } void readFoo() { Foo f = theFoo; // use f... } 

我不关心我的读者是否看到旧的或新的Foo,但是我需要看到完全初始化的对象。 IIUC,Java规范说,在这里没有内存屏障,我可能会看到一个fb被初始化的对象,但是还没有被提交到内存。 我的程序是一个现实世界的程序,它迟早会将内容提交到内存中,所以我不需要马上将新的值赋给内存(尽pipe它不会造成伤害)。

你认为什么是实现内存屏障最可读的方式? 如果需要的话,我愿意为了可读性而付出一些性能价格。 我想我可以把这个任务同步到Foo,这个工作就可以了,但是我不确定读这个代码的人很明显为什么这么做。 我也可以同步新的Foo的整个初始化,但是这会引入更多的实际需要的locking。

你怎么写它,使其尽可能可读?
奖金赞成Scala版本:)

原始问题的简短答案

  • 如果Foo是不可变的,那么简单地使字段最终将确保所有线程的字段的完全初始化和一致的可见性,而不pipe同步。
  • 无论Foo是不可变的,通过volatile theFooAtomicReference<Foo> theFoo足以确保写入其字段对任何通过theFoo引用读取的线程都是可见的
  • 使用一个明确的分配给读者线程永远不会保证看到任何更新
  • 在我看来,基于JCiP,“最可读的方式来实现内存障碍”是AtomicReference<Foo> ,显式同步在第二,使用volatile在第三
  • 可悲的是,我没有在斯卡拉提供

你可以使用volatile

我怪你。 现在我迷上了,我已经打破了JCiP ,现在我想知道我写的任何代码是否正确。 上面的代码片段实际上可能是不一致的。 (编辑:请参阅下面有关安全发布的部分,通过volatile。) 阅读线程也可以看到陈旧的(在这种情况下,无论ab的默认值是多less)无限的时间。 您可以执行以下任一操作来介绍发生之前的边缘:

  • 通过volatile发布,创build一个相当于monitorenter (读端)或monitorexit (写端)
  • 发布前使用final字段并在构造函数中初始化值
  • 将新值写入到theFoo对象时引入一个同步块
  • 使用AtomicInteger字段

这些解决了写入顺序(并​​解决了它们的可见性问题)。 那么你需要解决新的theFoo参考的可见性。 在这里, volatile是恰当的 – JCiP在3.1.4节“易变的variables”中说,(这里variablestheFoo ):

只有满足以下所有条件时,才可以使用volatilevariables:

  • 写入variables不取决于其当前值,也可以确保只有单个线程更新该值;
  • variables不参与与其他状态variables的不variables; 和
  • 正在访问variables时,locking不需要任何其他原因

如果你这样做,你是金的:

 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... } 

直截了当,可读

这个和其他线程上的几个人(感谢@John V )指出,有关这些问题的权威人士强调同步行为和假设的文档的重要性。 JCiP详细介绍了这一点,提供了一组可用于文档和静态检查的注释 ,您也可以查看JMM Cookbook中有关特定行为的指标,这些指标需要文档和指向适当参考的链接。 Doug Lea还编写了一些需要考虑并发行为时要考虑的问题清单。 文档是适当的,特别是因为担心,怀疑和围绕并发问题的困惑(关于SO: “是否有java并发玩世不恭?” )。 此外,像FindBugs这样的工具现在正在提供静态检查规则,以通知违反JCiP注释语义,如“Inconsistent Synchronization:IS_FIELD-NOT_GUARDED” 。

除非你认为自己有理由不这样做,否则最好是使用@Immutable@GuardedBy注解来处理最可读的解决scheme,像这样(谢谢,@Burleigh Bear)。

 @Immutable class 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

首先,请注意,这个问题与这里的问题有关,但在SO上多次提到:

  • 什么时候使用volatile?
  • 你有没有在Java中使用volatile关键字
  • 对于用什么“易变”
  • 使用volatile关键字
  • Java易失性布尔与AtomicBoolean

其实,googlesearch: “site:stackoverflow.com + java + volatile + keyword”返回355个不同的结果。 volatile使用充其量是一个不稳定的决定。 什么时候适合? JCiP给出了一些抽象指导(上面引用)。 我会在这里收集一些更实用的指导方针:

  • 我喜欢这个答案 :“ volatile可以用来安全地发布不可变的对象”,它巧妙地封装了应用程序员可能期望的大部分使用范围。
  • @ mdma的答案在这里 :“ volatile在无锁algorithm中最有用”总结了另一类用途 – 特殊目的,无锁algorithm,这些algorithm对性能非常敏感,值得专家仔细分析和validation。
  • 通过volatile进行安全发布

    接下来是@Jed Wesley-Smith ,看起来volatile现在提供了更强的保证(自JSR-133以来),而早期的断言“只要发布的对象是不可变的,你就可以使用volatile ”就足够了,但也许并不是必须的。

    查看JMM常见问题解答,两个条目最终字段如何在新的JMM下工作? 什么不稳定呢? 是不是真的在一起处理,但我认为第二个给我们什么我们需要:

    不同之处在于,现在不再那么容易对他们周围的正常字段进行重新sorting。 写入易失性字段具有与显示器发行版相同的记忆效应,从易失性字段读取与监视器获取的记忆效应相同。 实际上,由于新的内存模型对易失性字段访问进行其他字段访问的重新sorting有更严格的限制,不pipe是否为volatile,当线程A写入易失性字段f时对于线程B来说是可见的任何东西在读取f时对线程B都是可见的。

    我会注意到,尽pipeJCiP有几处重读,但在Jed指出之前,那里的相关文字并没有跳出来。 它在页面上。 38,第3.1.4节,它说或多或less与前面的引用相同的东西 – 发布的对象只需要有效的不可变,不需要final字段,QED。

    老的东西,保持责任

    一个评论:为什么newAnewB不能成为构造函数的参数? 那么你可以依靠发布规则的构造函数…

    此外,使用AtomicReference可能会消除任何不确定性(并且可能会根据您在其他课程中需要完成的工作来为您提供其他好处…)另外,比我更聪明的人可以告诉您, volatile是否会解决这个问题,但是对我来说总是显得神秘 …

    在进一步的回顾中,我相信上面的@Burleigh Bear的评论是正确的—(编辑:见下) 你实际上不必担心在这里乱序sorting,因为你正在发布一个新的对象theFoo 虽然另一个线程可能会看到JLS 17.11中描述的newAnewB不一致的值, newA不会发生,因为在另一个线程获取f = new Foo()实例的引用之前,它们将被提交到内存你创造了…这是安全的一次性出版物。 另一方面,如果你写了

     void updateFoo(int newA, int newB) { f = new Foo(); theFoo = f; fa = newA; fb = newB; } 

    但在这种情况下,同步问题是相当透明的,订购是您最担心的问题。 有关volatile的一些有用的指导,请参阅这篇developerWorks文章 。

    但是,您可能会遇到这样的问题,即单独的读者线程可能会在无限次的时间内看到theFoo的旧值。 实际上,这很less发生。 但是,JVM可能被允许在另一个线程的上下文中cachingtheFoo引用的值。 我非常确定将volatile标记为volatile将解决这个问题,就像任何types的同步器或AtomicReference

    拥有最终的a和b字段的不可变Foo解决了默认值的可见性问题,但是使得这个variables变得不稳定。

    就我个人而言,我喜欢拥有不可改变的价值类别,因为它们很难被误用。