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

Jea*_*ean 26 java concurrency scala

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

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...
}
Run Code Online (Sandbox Code Playgroud)

我至少不在乎我的读者是看到旧的还是新的Foo,但是我需要看到一个完全初始化的对象.IIUC,Java规范说没有HERE中的内存屏障,我可能会看到一个fb初始化但尚未提交到内存的对象.我的程序是一个真实世界的程序,它迟早会将内容提交到内存中,所以我不需要立即将新的值ofFoo提交到内存中(虽然它不会受到伤害).

您认为实现内存屏障最可读的方式是什么?如果需要,我愿意为了可读性而付出一点性能价格.我想我可以将赋值同步到Foo,这样可行,但是我不确定读取代码的人为什么这么做是非常明显的.我还可以同步新Foo的整个初始化,但这会引入更多实际需要的锁定.

你会如何编写它以使其尽可能可读?
对Scala版本的奖励荣誉:)

and*_*soj 45

对原始问题的简短回答

  • 如果Foo是不可变的,只需将字段设为最终将确保完全初始化和对所有线程的字段的一致可见性,而不管同步如何.
  • 无论是否Foo是不可变的,通过volatile theFooAtomicReference<Foo> theFoo足以确保通过theFoo引用读取的任何线程可以看到对其字段的写入是可见的
  • 使用普通赋值theFoo,读者线程永远不会保证看到任何更新
  • 在我看来,基于JCiP,"实现内存屏障最可读的方式"是AtomicReference<Foo>,显式同步排在第二位,使用volatile进入第三位
  • 可悲的是,我在Scala中没有任何东西可以提供

您可以使用 volatile

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

  • 发布via volatile,创建一个相当于monitorenter(读取侧)或monitorexit(写入侧)的before-before边缘
  • final在发布之前使用字段并初始化构造函数中的值
  • 在将新值写入theFooobject 时引入synchronized块
  • 使用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...
}
Run Code Online (Sandbox Code Playgroud)

直截了当,易读

关于这个和其他线程的几个人(感谢@John V)注意到这些问题的权威人士强调了同步行为和假设的文档记录的重要性.JCiP详细讨论了这一点,提供了一可用于文档和静态检查的注释,您还可以查看JMM Cookbook,了解有关需要文档和指向相应引用的链接的特定行为的指标.Doug Lea还准备了记录并发行为时要考虑的问题列表.文档是合适的,特别是因为对并发问题的关注,怀疑和混淆(在SO上:"java兼并玩世不恭是不是太过分了?" ).此外,像FindBugs这样的工具现在提供静态检查规则来注意违反JCiP注释语义,例如"Inconsistent Synchronization: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;}
  // use f...
}
Run Code Online (Sandbox Code Playgroud)

或者,可能,因为它更清洁:

static AtomicReference<Foo> theFoo;

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

什么时候适合使用 volatile

首先,请注意这个问题与此处的问题有关,但在SO上已经多次解决过:

事实上,谷歌搜索:"site:stackoverflow.com + java + volatile + keyword"返回355个不同的结果.使用volatile至多是一个不稳定的决定.什么时候合适?JCiP提供了一些抽象指导(如上所述).我会在这里收集一些更实用的指南:

  • 我喜欢这个答案:" volatile可以用来安全地发布不可变对象",它巧妙地封装了应用程序员可能期望的大部分使用范围.
  • @mdma的回答:" volatile在无锁算法中最有用"总结了另一类用途 - 特殊目的,无锁算法,这些算法对性能敏感,值得专家仔细分析和验证.

  • 安全发布通过volatile

    关注@Jed Wesley-Smith,现在似乎volatile提供了更强的保证(因为JSR-133),而早期的断言"你可以使用,volatile只要发布的对象是不可变的"就足够了,但也许没有必要.

    查看JMM常见问题解答,两个条目最终字段如何在新JMM下工作?挥发物有什么作用?并没有真正处理在一起,但我认为第二个给了我们所需要的东西:

    不同之处在于现在不再那么容易重新排序它们周围的正常字段访问.写入易失性字段与监视器释放具有相同的记忆效应,从易失性字段读取具有与监视器获取相同的记忆效应.实际上,因为新的存储器模型对具有其他字段访问(易失性或非易失性)的易失性字段访问的重新排序施加了更严格的约束,所以当线程A写入易失性字段f时线程A可见的任何内容在读取f时对线程B可见.

    我会注意到,尽管对JCiP进行了多次重新阅读,但在杰德指出之前,相关文本并没有向我推销.它在p.38,第3.1.4节,它与前面的引用或多或少相同 - 发布的对象只需要有效不可变,不需要final字段,QED.

    较旧的东西,保持责任

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

    此外,使用一个AtomicReference可能会消除任何不确定性(并可能会给你带来其他好处,取决于你在课堂其他部分需要做什么......)另外,比我聪明的人可以告诉你是否volatile可以解决这个问题,但它对我来说似乎总是神秘莫测 ......

    在进一步的评论中,我相信@Burleigh Bear上面的评论是正确的---(编辑:见下文)你实际上不必担心这里的无序排序,因为你要发布一个新的对象theFoo.而另一个线程可以想见,看到不一致的值newA,并newB在JLS 17.11描述,因为其他线程得到一个参考的阿霍德新之前,他们将被提交到存储器中,可以发生在这里f = new Foo()你已经创建的实例...这是安全的一次性出版物.另一方面,如果你写了

    void updateFoo(int newA, int newB) {
      f = new Foo(); theFoo = f;     
      f.a = newA; f.b = newB;
    }
    
    Run Code Online (Sandbox Code Playgroud)

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

    但是,您可能会遇到一个问题,即单独的读取器线程可以看到theFoo无限时间内的旧值.在实践中,这种情况很少发生.但是,可以允许JVM theFoo在另一个线程的上下文中缓存引用的值.我确信标记theFoovolatile将解决这个问题,如将任何类型的同步或AtomicReference.