当前位置: 首页 > 面试题库 >

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

萧繁
2023-03-14
问题内容

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

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规范说,这里没有内存障碍,我可能会看到一个对象,其中fb已初始化,但fa尚未提交给内存。我的程序是一个现实世界的程序,迟早会将内容提交到内存中,因此,我不需要立即将theFoo的新值提交到内存中(尽管这不会造成伤害)。

您认为实现内存屏障的最容易理解的方法是什么?为了便于阅读,我愿意付出一点性能代价。我想我可以将赋值同步到Foo,这样就可以了,但是我不确定对于阅读代码的人来说为什么这样做很明显。我还可以同步新Foo的整个初始化过程,但这会引入更多实际需要的锁定。

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


问题答案:

原始问题的简短答案

  • 如果Foo是不变的,则只需将字段定为final将确保完全初始化,并且字段对所有线程的一致性可见性,而与同步无关。
  • 是否Foo是不可变的,通过volatile theFoo或进行发布,AtomicReference<Foo> theFoo足以确保通过theFoo引用读取的任何线程都可以看到对其字段的写入
  • 使用对的简单分配theFoo永远不能 保证阅读器线程看到任何更新
  • 在我看来,基于JCiP,是“落实记忆障碍最可读的方式” AtomicReference<Foo>,用明确的同步中第二次来,和使用的volatile第三来临
  • 可悲的是,我在斯卡拉没有任何提供

您可以使用 volatile

我怪你。现在我迷上了,我已经分解了JCiP,现在我想知道我编写的任何代码是否正确。上面的代码片段实际上可能是不一致的。(编辑:请参见通过挥发下方安全发布的部分。)
读线程可以 同时 看到陈旧的(在这种情况下,无论默认值 ab均)为无限时间。 您可以执行以下任一操作以引入“先发生边缘”:

  • 通过进行发布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)。

@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

实际上,谷歌搜索:“ 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。

较旧的内容,用于问责制

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

另外,使用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在另一个线程的上下文中缓存引用的值。我非常确定标记theFoovolatile可以解决此问题,任何类型的同步器或都可以解决AtomicReference



 类似资料:
  • 问题内容: 内存屏障可确保数据缓存保持一致。但是,是否可以保证TLB保持一致? 我看到一个问题,即在线程之间传递MappedByteBuffer时,JVM(java 7更新1)有时会因内存错误(SIGBUS,SIGSEG)而崩溃。 例如 没有Thread.yield(),我有时会在force(),put()和C的memcpy()中崩溃,所有这些都表明我试图非法访问内存。使用Thread.yield

  • 问题内容: 最近,我正在阅读一些Linux内核空间代码,我看到了 该代码段的语义是什么?是否确保#1在#3之前由#2执行。但是我有点乱,因为 #A 在64位平台上,atomic64_read宏扩展为 在32位平台中,将其转换为使用锁 cmpxchg8b 。我认为这两个具有相同的语义,对于64位版本,我认为这意味着: all-or-nothing ,我们可以排除地址未对齐且字长大于CPU本机字长的情

  • 引子 这篇教程旨在帮助开发者快速上手 Rax iOS 上的无障碍开发。 无障碍,即「accessibility」(常常缩写成「a11y」),是相对有障碍访问而言的,常见的有障碍访问场景有两类。 一种是用户因为生理缺陷,没有能力按正常的交互方式访问,举几个例子: 视障人士看不见或看不清,无法感受手淘上的信息,动效,氛围; 听障人士听不见或听不清,无法听到音乐以及视频的语音部分; 老年人视力和听力的退

  • 无障碍性(a11y) 当你设计一款扩展,需要让扩展对于诸如视觉缺陷,失聪,行动不便的残疾人没有使用障碍。 所有人 — 不仅仅是有特殊需求的人 — 都应该能从那些无障碍扩展所提供的相应模式中获益。例如,键盘的快捷键对于盲人,灵敏度较差的那些人非常重要,然而他们也能提高高级用户在无鼠标状态下的工作效率。 字幕和手抄本提供了聋人获取影音内容的通道,然而他们对语言学习者也非常有用。 人们可以通过各种方式和