Java VM 上的内存障碍和编码风格 [英] Memory barriers and coding style over a Java VM

查看:23
本文介绍了Java VM 上的内存障碍和编码风格的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我有一个静态复杂对象,它由线程池定期更新,并在长时间运行的线程中或多或少地连续读取.对象本身始终是不可变的,并反映某事物的最新状态.

Suppose I have a static complex object that gets periodically updated by a pool of threads, and read more or less continually in a long-running thread. The object itself is always immutable and reflects the most recent state of something.

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

我一点也不关心我的读者看到的是旧的还是新的 Foo,但是我需要看到一个完全初始化的对象.IIUC,Java 规范说,如果在 HERE 中没有内存屏障,我可能会看到 f.b 已初始化但 f.a 尚未提交到内存的对象.我的程序是一个真实世界的程序,迟早会向内存提交内容,所以我不需要立即将 theFoo 的新值实际提交到内存中(尽管它不会有什么坏处).

I do not care in the least whether my reader sees the old or the new Foo, however I need to see a fully initialized object. IIUC, The Java spec says that without a memory barrier in HERE, I may see an object with f.b initialized but f.a not yet committed to memory. My program is a real-world program that will sooner or later commit stuff to memory, so I don't need to actually commit the new value of theFoo to memory right away (though it wouldn't hurt).

您认为实现内存屏障的最易读的方式是什么?如果需要,我愿意为可读性付出一点性能代价.我想我可以只将分配同步到 Foo 就可以了,但是我不确定对于阅读代码的人来说为什么我这样做很明显.我也可以同步新 Foo 的整个初始化,但这会引入更多实际需要的锁定.

What do you think is the most readable way to implement the memory barrier ? I am willing to pay a little performance price for the sake of readability if need be. I think I can just synchronize the assignment to Foo and that would work, but I'm not sure it's very obvious to someone reading the code why I do that. I could also synchronize the whole initialization of the new Foo, but that would introduce more locking that actually needed.

您将如何编写它以使其尽可能具有可读性?
Scala 版本的额外荣誉:)

How would you write it so that it's as readable as possible ?
Bonus kudos for a Scala version :)

推荐答案

对原始问题的简短回答

  • 如果 Foo 是不可变的,只需将字段设为 final 即可确保所有线程的完整初始化和一致的字段可见性,而无需考虑同步.
  • 无论Foo 是否不可变,通过volatile theFooAtomicReference 发布;theFoo 足以确保通过 theFoo 引用
  • 读取的任何线程都可以看到对其字段的写入
  • 使用对 theFoo 的简单赋值,阅读器线程永远保证看到任何更新
  • 在我看来,基于 JCiP,实现内存屏障的最易读的方式"是 AtomicReference,其次是显式同步,并使用 volatile 排在第三位
  • 遗憾的是,我在 Scala 中没有什么可提供的
  • Short Answers to the Original Question

    • If Foo is immutable, simply making the fields final will ensure complete initialization and consistent visibility of fields to all threads irrespective of synchronization.
    • Whether or not Foo is immutable, publication via volatile theFoo or AtomicReference<Foo> theFoo is sufficient to ensure that writes to its fields are visible to any thread reading via theFoo reference
    • Using a plain assignment to theFoo, reader threads are never guaranteed to see any update
    • In my opinion, and based on JCiP, the "most readable way to implement the memory barrier" is AtomicReference<Foo>, with explicit synchronization coming in second, and use of volatile coming in third
    • Sadly, I have nothing to offer in Scala
    • 我怪你.现在我迷上了,我已经打破了 JCiP,现在我想知道我写过的任何代码是否正确.事实上,上面的代码片段可能不一致.(请参阅下面关于通过 volatile 进行安全发布的部分.)阅读线程可能看到过时(在这种情况下,无论 ab 是)无限时间. 您可以执行以下操作之一来引入发生之前的边缘:

      I blame you. Now I'm hooked, I've broken out JCiP, and now I'm wondering if any code I've ever written is correct. The code snippet above is, in fact, potentially inconsistent. ( see the section below on Safe publication via volatile.) The reading thread could also see stale (in this case, whatever the default values for a and b were) for unbounded time. You can do one of the following to introduce a happens-before edge:

      • 通过 volatile 发布,这会创建一个与 monitorenter(读取端)或 monitorexit(写入端)等价的发生前边
      • 使用 final 字段并在发布前初始化构造函数中的值
      • 在将新值写入 theFoo 对象时引入同步块
      • 使用 AtomicInteger 字段
      • Publish via volatile, which creates a happens-before edge equivalent to a monitorenter (read side) or monitorexit (write side)
      • Use final fields and initialize the values in a constructor before publication
      • Introduce a synchronized block when writing the new values to theFoo object
      • Use AtomicInteger fields

      这些解决了写入顺序(并​​解决了它们的可见性问题).然后您需要解决新的 theFoo 引用的可见性.在这里,volatile 是合适的——JCiP 在第 3.1.4 节易变变量"中说,(这里的 变量theFoo):

      These gets the write ordering solved (and solves their visibility issues). Then you need to address visibility of the new theFoo reference. Here, volatile is appropriate -- JCiP says in section 3.1.4 "Volatile variables", (and here, the variable is theFoo):

      • 写入变量不依赖于它的当前值,或者您可以确保只有一个线程会更新该值;
      • 变量不参与与其他状态变量的不变量;和
      • 在访问变量时不需要出于任何其他原因锁定

      如果你做到以下几点,你就是金子:

      If you do the following, you're golden:

      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 注释语义的行为,比如 "不一致的同步:IS_FIELD-NOT_GUARDED".

      Straightforward and Readable

      Several folks on this and other threads (thanks @John V) note that the authorities on these issues emphasize the importance of documentation of synchronization behavior and assumptions. JCiP talks in detail about this, provides a set of annotations that can be used for documentation and static checking, and you can also look at the JMM Cookbook for indicators about specific behaviors that would require documentation and links to the appropriate references. Doug Lea has also prepared a list of issues to consider when documenting concurrency behavior. Documentation is appropriate particularly because of the concern, skepticism, and confusion surrounding concurrency issues (on SO: "Has java concurrency cynicism gone too far?"). Also, tools like FindBugs are now providing static checking rules to notice violations of JCiP annotation semantics, like "Inconsistent Synchronization: IS_FIELD-NOT_GUARDED".

      除非您认为有理由不这样做,否则最好使用 @Immutable@GuardedBy 注释.

      Until you think you have a reason to do otherwise, it's probably best to proceed with the most readable solution, something like this (thanks, @Burleigh Bear), using the @Immutable and @GuardedBy annotations.

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

      或者,可能,因为它更干净:

      or, possibly, since it's cleaner:

      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 上多次解决:

      When is it appropriate to use volatile

      First, note that this question pertains to the question here, but has been addressed many, many times on SO:

      其实google一搜:"站点:stackoverflow.com +java +volatile +keyword" 返回 355 个不同的结果.使用 volatile 充其量是一个易变的决定.什么时候合适?JCiP 给出了一些抽象的指导(上面引用了).我会在这里收集一些更实用的指南:

      In fact, a google search: "site:stackoverflow.com +java +volatile +keyword" returns 355 distinct results. Use of volatile is, at best, a volatile decision. When is it appropriate? The JCiP gives some abstract guidance (cited above). I'll collect some more practical guidelines here:


      跟进 @Jed Wesley-Smith,看来 volatile 现在提供了更强的保证(自 JSR-133 起),早期的断言如果发布的对象是不可变的,您可以使用 volatile"就足够了,但可能没有必要.

      Following up on @Jed Wesley-Smith, it appears that volatile now provides stronger guarantees (since JSR-133), and the earlier assertion "You can use volatile provided the object published is immutable" is sufficient but perhaps not necessary.

      查看JMM FAQ,两个条目final 字段在新的 JMM 下如何工作?volatile 有什么作用? 并没有真正一起处理,但我认为第二个给了我们我们需要的:

      Looking at the JMM FAQ, the two entries How do final fields work under the new JMM? and What does volatile do? aren't really dealt with together, but I think the second gives us what we need:

      不同的是现在没有了更长那么容易重新排序正常字段他们周围的访问.写入一个volatile 字段具有相同的内存作为监听版本的效果,以及从易失性字段读取具有与显示器相同的记忆效应获得.实际上,因为新内存模型放置更严格对 volatile 重新排序的限制与其他字段的字段访问访问,无论是否不稳定,任何东西线程 A 可见时写入易失性字段 f 变为线程 B 在读取 f 时可见.

      The difference is that it is now no longer so easy to reorder normal field accesses around them. Writing to a volatile field has the same memory effect as a monitor release, and reading from a volatile field has the same memory effect as a monitor acquire. In effect, because the new memory model places stricter constraints on reordering of volatile field accesses with other field accesses, volatile or not, anything that was visible to thread A when it writes to volatile field f becomes visible to thread B when it reads f.

      我会注意到,尽管对 JCiP 进行了多次重读,但直到 Jed 指出它的相关文本并没有跳出来给我.它在第.38,第 3.1.4 节,它或多或少与前面的引用相同——发布的对象只需要有效地不可变,不需要 final 字段,QED.

      I'll note that, despite several rereadings of JCiP, the relevant text there didn't leap out to me until Jed pointed it out. It's on p. 38, section 3.1.4, and it says more or less the same thing as this preceding quote -- the published object need only be effectively immutable, no final fields required, QED.

      一个评论:newAnewB 不能作为构造函数的参数的任何原因?然后就可以依赖构造函数的发布规则了...

      One comment: Any reason why newA and newB can't be arguments to the constructor? Then you can rely on publication rules for constructors...

      此外,使用 AtomicReference 可能会消除任何不确定性(并且可能会为您带来其他好处,具体取决于您在课程其余部分需要完成的工作...)我可以告诉你 volatile 是否可以解决这个问题,但它似乎总是 对我来说很神秘...

      Also, using an AtomicReference likely clears up any uncertainty (and may buy you other benefits depending on what you need to get done in the rest of the class...) Also, someone smarter than me can tell you if volatile would solve this, but it always seems cryptic to me...

      在进一步的审查中,我相信上面@Burleigh Bear 的评论是正确的---(见下文) 你实际上不必担心这里的顺序排序,因为你正在向 theFoo 发布一个新对象.虽然另一个线程可能会看到 JLS 17.11 中描述的 newAnewB 不一致的值,但这不会发生在这里,因为它们将在另一个线程获取之前提交到内存保留对您创建的新 f = new Foo() 实例的引用...这是安全的一次性发布.另一方面,如果你写

      In further review, I believe that the comment from @Burleigh Bear above is correct --- ( see below) you actually don't have to worry about out-of-sequence ordering here, since you are publishing a new object to theFoo. While another thread could conceivably see inconsistent values for newA and newB as described in JLS 17.11, that can't happen here because they will be committed to memory before the other thread gets ahold of a reference to the new f = new Foo() instance you've created... this is safe one-time publication. On the other hand, if you wrote

      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 也是如此.

      However, you may have an issue where separate reader threads can see the old value for theFoo for unbounded amounts of time. In practice, this seldom happens. However, the JVM may be allowed to cache away the value of the theFoo reference in another thread's context. I'm quite sure marking theFoo as volatile will address this, as will any kind of synchronizer or AtomicReference.

      这篇关于Java VM 上的内存障碍和编码风格的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆