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

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

问题描述

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

  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尚未提交到内存。我的程序是一个真实世界的程序,将迟早提交内存到内存,所以我不需要实际上提交新的值的内存到内存马上(虽然它不会伤害)。



你认为实现内存屏障的最可读的方法是什么?我愿意支付一点性能价格为了可读性,如果需要的话。我想我可以只是同步分配到Foo,这将工作,但我不知道这是非常明显的人阅读的代码为什么我这样做。我也可以同步新的Foo的整个初始化,但这将引入更多的锁定,实际需要。



如何写它,使其尽可能可读?

Scala版本的奖励:)

解决方案




  • 如果 Foo 是不可变的,只需将fields设置为final即可确保完全初始化, c> c> 是不可变的,通过发布volatile theFoo code>或 AtomicReference< Foo> Foo 足以确保对其字段的写入对通过 theFoo 引用读取的任何线程可见

  • 使用 theFoo 的简单赋值,读者线程永远不会保证看到任何更新

  • 在我看来,并且基于JCiP,最可读的实现存储器屏障的方式是 AtomicReference ,其中显式同步进入第二,并且使用 volatile 第三个


b $ b

你可以使用 volatile



现在我被钩了,我已经打破了 JCiP ,现在我想知道是否有任何代码, ve写的是正确的。上面的代码段实际上可能不一致。 (编辑:参见下面关于通过volatile的安全发布部分。)读取线程还可以看到stale(在这种情况下,不管 a 您可以执行以下操作之一来引入一个发生前的边缘:

$ b b
$ b

  • 通过 volatile 发布,创建一个等于 monitorenter (read side)或 monitorexit (写入)

  • 使用 final 字段并在发布之前初始化构造函数中的值

  • 在将新值写入 theFoo 对象

  • 使用 AtomicInteger 字段



这些获得写入顺序解决(并解决他们的可见性问题)。然后,您需要解决新的 theFoo 引用的可见性。这里, volatile 是合适的 - JCiP 在3.1.4易失性变量(这里,变量 theFoo ):


只有在满足以下所有条件时才可以使用volatile变量:

  • 对变量的写入不依赖于其当前值,确保只有一个线程永远更新该值;

  • 该变量不参与其他状态变量的不变量;


  • 正在访问变量时,不需要锁定任何其他原因。

如果你做以下事情,你就是金牌:

  class Foo {
/ /结果是这些字段可能不是最终的,与volatile发布,
//值将在新的JMM下看到
final int a,b;
Foo(final int a; final int b)
{this.a = a; this.b = b; }
}

//不可变的这里,单独的线程调用readFoo()
//可能永远看不到新的theFoo值,由线程A
静态变量Foo;
void updateFoo(int newA,int newB){
f = new Foo(newA,newB);
theFoo = f;
}
void readFoo(){
final Foo f = theFoo;
//使用f ...
}



h2>

此线程和其他线程上的几个人(感谢 @John V )注意,这些问题的权威部门强调了同步行为和假设的文档的重要性。 JCiP详细讨论了这一点,提供了一个一组注释,可用于文档和静态检查,您也可以查看 JMM手册,了解有关需要文档和链接的具体行为的指标到适当的引用。 Doug Lea还准备了记录并发行为时需要考虑的问题列表。文档是适当的,特别是因为担心,怀疑和围绕并发问题的混乱(在SO: 有java并发玩世不恭去了吗?)。此外, FindBugs 等工具现在提供静态检查规则,以注意违反JCiP注释语义的情况,例如不一致同步:IS_FIELD-NOT_GUARDED



直到您认为您有理由否则,最好是继续使用最易读的解决方案,像这样(感谢@Burleigh Bear),使用 @Immutable @GuardedBy 注释。

  @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;}
//使用f ...
}

或者,因为它更干净:

  static AtomicReference< Foo> oo 

void updateFoo(final int newA,final int newB){
theFoo.set(new Foo(newA,newB)); }
void readFoo(){Foo f = theFoo.get(); ...}



何时适合使用 volatile



首先,请注意,此问题与此处的问题相关,但在SO中已解决了许多问题:





事实上,google搜索:\"site:stackoverflow.com + java + volatile + keyword返回355个不同的结果。使用 volatile 最多是一个易变的决定。什么时候适当? JCiP给出了一些抽象指导(如上所述)。我将在这里收集一些更实用的指南:




  • 我喜欢 volatile 可以用来安全地使用

  • @ mdma的 answer here volatile is最有用的无锁算法总结了另一类用途 - 特殊目的,无锁算法,其足够性能敏感,以便由专家仔细分析和验证。






  • 通过挥发性安全出版



    @Jed Wesley-Smith ,似乎 volatile 现在提供了更强的保证(自JSR-133),而早期的断言你可以使用 volatile 提供已发布的对象是不可变的足够但可能不是必要的。 p>

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


    区别是它现在没有
    更长,所以很容易重新排序正常字段
    访问它们。写入
    volatile字段具有与监视器版本相同的内存
    效果,而从易失性字段读取的
    的效果与
    监视器的内存效果相同。实际上,因为新的
    内存模型将更严格的
    约束重新排序volatile
    字段访问与其他字段
    访问,volatile或不可用,任何
    是可见的到线程A当它
    写入易失性字段f变成
    当线程B读取f时可见。


    我会注意到,尽管JCiP的几次重读,相关的文本没有跳跃到我,直到Jed指出。它在p。 38,section 3.1.4,并且它或多或少地说明了与前面的引用相同的东西 - 发布的对象只需要有效地不可变,不需要 final 字段,QED 。



    旧的东西,保留为问责



    一个评论:任何原因 newA newB 不能是构造函数的参数?然后你可以依赖于构造函数的发布规则...



    此外,使用 AtomicReference 而且,比我更聪明的人可以告诉你 volatile 会不会带来其他的好处,取决于你需要在类的其余部分做什么。解决这个问题,但它似乎总是对我隐瞒。 。



    进一步的评论中,我相信上面@Burleigh Bear的评论是正确的---(编辑:见下文)你实际上没有在这里担心乱序排序,因为你正在向 theFoo 发布一个新对象。虽然另一个线程可以看到JLS 17.11中描述的 newA newB 的不一致的值因为他们将被提交到内存之前,其他线程得到一个引用的新的 f = new Foo()你创建的实例...这是安全的时间出版。另一方面,如果你写

      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


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

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

    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.

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

    解决方案

    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

    You can use volatile

    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. (Edit: 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:

    • 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

    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):

    You can use volatile variables only when all the following criteria are met:
    • Writes to the variable do not depend on its current value, or you can ensure that only a single thread ever updates the value;
    • The variable does not participate in invariants with other state variables; and
    • Locking is not required for any other reason while the variable is being accessed

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

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

    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(); ... }
    

    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:

    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:

  • I like this answer: "volatile can be used to safely publish immutable objects", which neatly encapsulates most of the range of use one might expect from an application programmer.
  • @mdma's answer here: "volatile is most useful in lock-free algorithms" summarizes another class of uses—special purpose, lock-free algorithms which are sufficiently performance sensitive to merit careful analysis and validation by an expert.

  • Safe Publication via 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.

    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:

    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.

    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.

    Older stuff, kept for accountability

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

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

    In further review, I believe that the comment from @Burleigh Bear above is correct --- (EDIT: 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;
    }
    

    But in that case the synchronization issues are fairly transparent, and ordering is the least of your worries. For some useful guidance on volatile, take a look at this developerWorks article.

    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天全站免登陆