为什么Java中的引用赋值是原子性操作?

25
据我所知,在64位JVM中,引用赋值是原子性的。我猜想JVM内部没有使用原子指针来模拟这一过程,否则就不需要原子引用(Atomic References)了。因此我的问题是:
Java/Scala规范是否保证原子引用赋值,并且保证其发生?
对于任何编译成JVM字节码的语言(例如Clojure、Groovy、JRuby、JPython等),是否隐含了原子引用赋值?
在不使用原子指针的情况下,如何使引用赋值是原子性的?

@JornVernee 我的意思是赋值而不是交换,感谢您的纠正。 - George
这可能会给出第一个线索:https://dev59.com/-n7aa4cB1Zd3GeqPxPmC - GhostCat
我已经尝试了...但是我一直在网上搜索如何实现getfield、putfiled和iastore,但我没有找到不需要深入研究openjvm代码的解释,而我并不熟练 :( - George
3个回答

31
首先,由于规范中是这样规定的,参考赋值是原子性的。此外,在64位体系架构上通常只使用64位引用,因此JVM实现者没有障碍来满足这个约束条件,因为原子的64位赋值在这里是免费的。
你的主要困惑源于这样的假设:附加的“原子引用”功能确实是这样的,因为它的名字如此。但是,AtomicReference类提供了更多功能,因为它封装了一个volatile引用,在多线程执行中具有更强的内存可见性保证。
具有原子引用更新并不一定意味着读取引用的线程也会看到关于该引用可达的对象字段的一致值。它所保证的只是你将读取null引用或有效的引用,该引用实际上是由某个线程存储的指向现有对象。如果您想要更多保证,您需要构造像同步、volatile引用或AtomicReference这样的结构。
AtomicReference还提供像compareAndSet或getAndSet这样的原子更新操作。使用内置语言结构,普通的引用变量不可能实现这些操作(但只能使用像AtomicReferenceFieldUpdater或VarHandle这样的特殊类)。

1
@George:设置AtomicReference可以保证在通过该AtomicReference发布对象之前对该对象进行的所有更新的可见性,而不是允许您在此之后进行更新。通过普通引用变量发布对象不会对更新做出任何保证,无论您是在之前还是之后进行更新。它甚至不能保证其他线程将看到新引用。 - Holger
2
“原子指针”并不一定是指AtomicReference中的“原子引用”。因此,有必要指出引用(即底层指针)原子的,但仍然不能匹配AtomicReference的功能。 - Holger
3
“发布”意味着将变量分配给其他线程可见。但是,当通过普通引用进行发布时,没有关于先前更新的可见性的保证。如果您编写point=new Point(42,100)并且point既不是final也不是volatile,则其他线程可能看到新引用,同时仍然看到默认值(0)对于xy,或者两者都是(在缺乏其他同步的情况下)。 - Holger
3
这不是关于一个较旧的实例;在赋值之前,引用可能已经是“null”,这并不重要。问题是,new Point(42,100) 不是一个原子操作。它创建一个新的 Point 实例,所有字段都处于它们的默认值(对于 int0),然后构造函数将被执行,它将把值 42 分配给 x,并将值 100 分配给 y,然后从单线程的角度来看,引用被分配给 point。另一个线程可能会看到这些操作的顺序不同,看到对新的 Point 实例的引用,但没有看到赋值的效果。 - Holger
2
这不适用于不可变对象。如果您将xy声明为final,它们就不会受到此类数据竞争的影响。或者,您可以将point声明为final,但这意味着整个操作是构造函数或初始化器的一部分。或者,将point声明为volatile可以确保其他线程在通过point引用看到新的Point实例时看到所有先前的更新(x=42; y=100;)。 - Holger
显示剩余14条评论

10

原文中提到了原子引用赋值。

引用的写入和读取始终是原子性的,无论它们是作为32位还是64位值实现的。

摘自《JSR-133:Java(TM)内存模型和线程规范》,第12节《对doublelong的非原子操作处理》,链接:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf


好的,这回答了第一个问题,但没有回答另外两个问题。说实话,我最感兴趣的是如果引用不是原子指针,这种行为会如何发生,这与如何实现JVM有关,而不是Java和/或JVM的规范。 - George
真实。我故意不在我不确定的事情上装聪明。我理解的方式是,这是JVM的规范,所以我希望它涵盖所有在JVM上运行的语言。而且我读到的方式是,它要求内部指针操作在原子级别上发生(至少从JVM外部看来是这样)。 - Ole V.V.
是的,但我怀疑那些并不是真正的原子指针操作,否则像比较和交换这样的操作也会使用标准引用进行原子操作。 - George
1
好答案 - 不错的发现;我投票支持。而且谢谢你留出了那个给我的回答留下空间的小部分;-) - GhostCat
3
这份 PDF 描述了 JSR133 提出的 Java 5 内存模型。为了证明它确实被收录在 Java 5 JLS 中并且至今仍在使用,最好链接到 JLS §17.7 页面。 - Holger
感谢你指出来,@Holger,我完全同意。那里的引用完全相同。 - Ole V.V.

3
作为其他答案所述,Java内存模型规定引用读写是原子的。
但当然,那是Java语言的内存模型。另一方面,无论我们谈论Java还是Scala或Kotlin等,最终都会编译成字节码
Java没有特殊的字节码指令。Scala最终使用完全相同的指令。
导致:该内存模型的属性必须在VM平台内部实现。因此,它们必须也适用于在平台上运行的其他语言。

当时编写那个内存模型的时候,Java VM 平台上只有 Java 语言。我想。 - GhostCat
当然,这是Java语言内存模型。但实际上,它描述了Java语言和Java虚拟机的行为,并且通常清楚地说明哪个是哪个。大多数情况下,它讨论的是虚拟机,特别是这一部分:“Java虚拟机可以原子地或分两部分执行对长整型和双精度浮点型的写入操作”。 - Alexey Romanov
4
有的。比如,javac 生成的代码从不使用 swap。在 Java 7 中,invokedynamic 指令也不被 Java 自身使用,这是该 JVM 版本的一个重要特性。 - Holger
@Holger 为了辩护自己,现在Java本身使用invokedynamic。:-p 不过我不知道swap。谢谢! - Jasper-M
2
在得出这个结论时需要记住的一件事是,只有当其他语言(即引用)的概念被编译成等效的字节码构件时才适用。例如,某种语言的引用可能与Java的对象引用相差甚远,无法匹配单独的字节码引用变量,而是一个复合数据结构。对于Java来说,不可能遇到一个具有尚未初始化vtable的对象引用,但这并不一定适用于另一种具有完全不同方法分派算法的语言。 - Holger
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接