为什么使用volatile会使long和double类型变为原子类型

7

我正在尝试学习Java中多线程的术语。如果我在以下文本中使用了错误的定义,请纠正我:

来自不同资源的发现

原子操作: 根据Java文档:

在编程中,原子操作是一次有效完成的操作。原子操作不能在中间停止:它要么完全发生,要么根本不发生。原子操作的任何副作用在操作完成之前都不可见。

这就是为什么读取或写入长整型或双精度变量不是原子操作的原因。因为它涉及两个操作,第一个是32位操作,第二个是32位读/写操作。此外,从上面的段落中,我理解如果我在方法上使用synchronized,它将使该方法成为原子方法(从理论上讲)。

易失变量:也来自Java文档:

这意味着对易失变量的更改始终对其他线程可见。更重要的是,它还意味着当线程读取易失变量时,它不仅看到易失变量的最新更改,而且看到导致更改的代码的副作用。

现在,根据Joshua Bloch的《Effective Java 2nd Edition》中提到的以下几点,也考虑volatile声明:

请考虑以下内容:

// Broken - requires synchronization!
private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
    return nextSerialNumber++;
}

该方法的状态由一个原子可访问的字段nextSerialNumber组成,该字段的所有可能值都是合法的。因此,不需要同步来保护其不变量。尽管如此,在没有同步的情况下,该方法将无法正常工作。

这是因为nextSerialNumber++不是原子操作,它执行读取、递增和写入操作。

我的总结

所以如果nextSerialNumber++不是原子操作,需要使用synchronize。那么为什么以下操作是原子操作且不需要同步呢?

private static volatile long someNumber = 0;

public static int setNumber(long someNumber) {
    return this.someNumber = someNumber;
}

我不明白的是,为什么在 doublelong 上使用 volatile 可以使其变成原子性?
因为 volatile 的作用只是确保当线程 B 尝试读取被线程 A 写入的 long 变量,而线程 A 只写入了 32 位,则当线程 B 访问资源时,它将获取到线程 A 写入的 32 位数字。但这并不能使它变成“原子性”,因为“原子性”这一术语的定义在 Java Doc 中有所解释。

3
你在这里只是自说自话。你在上一个段落的第一句话所做的陈述只是你做出的错误假设,而不是从规范参考中得出的声明。JLS 17.4指出:“对volatile字段的写入发生在对该字段的每次后续读取之前”。 - user207421
2
有一个更有说服力的JLS参考文献...请看回答。 - Stephen C
1
@StephenC 确实,发现得不错。 - user207421
nextSerialNumber++ 是对 nextSerialNumber 的两个不同操作序列,中间至少有一个其他操作:它必须将变量的原始值提取到一个“工作位置”,然后增加工作副本,最后将结果存储回变量。另一方面,this.someNumber = someNumber 只对 this.someNumber 执行 一个 操作:它存储新值。 - Solomon Slow
2个回答

14
我不明白的是,为什么在double或long上使用volatile会使其成为原子操作?
如果不使用volatile关键字,您可能会读取一个由一个线程写入的double或long的前32位,和另一个线程写入的后32位,这称为单词撕裂,显然不是原子性操作。
volatile关键字确保这种情况不会发生。您读取的64位值将是一个线程写入的值,而不是多个线程写入结果的奇怪值。这就是指这些类型由于volatile关键字而变得具有原子性的含义。
无论类型是64位还是32位,volatile关键字都无法使类似于x++的操作成为原子操作,因为它是一个复合操作(读取+增量+写入),而不是简单的写入。涉及到的操作可能会被其他线程的操作插入。volatile关键字无法使此类复合操作成为原子操作。
那么,如果nextSerialNumber ++不是原子操作,并且需要同步,请问以下操作为什么是原子操作并且不需要同步?
private static volatile long someNumber = 0;

public static int setNumber(long someNumber) {
    return this.someNumber = someNumber;
}

nextSerialNumber++需要同步是因为它是一个复合操作,因此不是原子操作。

this.someNumber = someNumber是原子性的,得益于this.someNumbervolatile的,而赋值操作也是原子性的,因为它是单一操作。因此没有必要进行同步。没有volatilethis.someNumber将不能以原子方式写入,因此需要同步。


谢谢您的回复。所以,如果我理解正确,当一个线程向一个volatile long或double写入时,另一个线程不能在写入过程中读取,直到写入完全完成。因此,它的行为类似于同步方法。是这样吗? - Amin
它的行为就像一个易失变量,这也正是它的本质。如果“volatile”允许读取Franken-值,那么它将完全无用。这里唯一的问题是你错误的假设。 - user207421
2
我认为这个答案非常令人困惑和误导。虽然没有使用volatile可能会出现字撕裂,但也可能以其他方式失败。暗示字撕裂是唯一的问题是具有误导性和混淆性的。volatile不仅确保不会发生字撕裂,而且还提供了针对任何可能的故障机制的特定保证,即使在我们完全不知道硬件的情况下也是如此。 - David Schwartz
x++ 这样的操作可以被设置为原子操作,但是 volatile 并不会这样做,因为 volatile 的设计目的并不是提供原子性,而是提供可见性和排序。它在需要可见性暗示原子性的情况下“碰巧”提供了原子性。但是它始终提供可见性,这就是它的作用。按照措辞,答案表明 volatile 在“可能的情况下”提供原子性,这是不正确的。 - David Schwartz
除了提问者可能会认为,没有人认为字撕裂是“问题”,甚至是“重要问题”。然而,这个问题特别询问了longdouble……以及撕裂。而且如果没有volatile(或其他形式的同步),确实会出现字撕裂。 - Stephen C
显示剩余4条评论

8
我不明白的是为什么在double或long上使用volatile会使其具有原子性?
原因在于,使用volatile与double或long一起使用会使它们具有原子性,因为JLS这样规定。
JLS (第17.7节) 规定:
“对于volatile long和double值的写入和读取始终是原子性的。”
注意:JLS是规范性的。就语言语义而言,javadocs不具有规范性。Bloch的《Effective Java》甚至不是Oracle的文档——它只是一篇(未经授权的)评论。

2
@moamzia 具体而言,《Effective Java》不是一本规范性的参考书。 - user207421
这甚至不是一个Oracle文档。 - Stephen C
我自己也厌倦了,特别是将其视为来自西奈山的一对石板。请参见此处 以获取示例。谈论先马后车。 - user207421
我相信发帖者实际上问的是为什么 volatile 会使 i++ 成为原子操作。答案是它不会,因为它是一个复合语句。 - sola
@sola - 我不同意。请再次阅读他的问题。他陈述了他理解 i++ 是非原子性的,无论 i 是否是 volatile - Stephen C

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