【问题标题】:why using volatile makes long and double atomic [duplicate]为什么使用 volatile 会使 long 和 double atomic [重复]
【发布时间】:2016-03-27 00:42:22
【问题描述】:

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

我从不同资源中的发现

原子动作: 根据 Java 文档:

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

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

易失性变量:同样来自 Java Doc:

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

现在,同样根据 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 位数字。这并不能使其成为 atomic,因为解释了术语 atomic 的定义在 Java 文档中。

【问题讨论】:

  • 你只是在这里和自己争论。您在最后一段的第一句话中所做的陈述只是您所做的错误假设,而不是来自规范性参考的陈述。 JLS 17.4 声明“对 volatile 字段的写入发生在该字段的每次后续读取之前”。
  • 有一个更有说服力的 JLS 参考资料……看答案。
  • @StephenC 确实,很好。
  • nextSerialNumber++ 是对nextSerialNumber 的两个不同操作的序列,其中至少还有一个其他操作:它必须从变量中获取原始值到工作位置 i>,然后它递增工作副本,然后将结果存储回变量中。另一方面,this.someNumber = someNumber 只对 this.someNumber 执行 一个 操作:它存储新值。

标签: java multithreading concurrency volatile


【解决方案1】:

我不明白为什么在 double 或 long 上使用 volatile 会使其成为原子?

如果不使用volatile 关键字,您可能会读取由一个线程写入的doublelong 的前32 位,以及由另一个线程写入的其他32 位,称为字撕裂 em>,显然不是原子的。

volatile 关键字确保不会发生这种情况。您读取的 64 位值将是由一个线程写入的值,而不是由多个线程写入的某个 Franken 值。这就是由于volatile 关键字,这些类型变得原子的意思。

volatile 关键字不能进行像x++ 这样的原子操作,无论类型如何(64 位或 32 位),因为它是复合操作(读取 + 递增 + 写入),而不是简单的写入。 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 时,另一个线程不能只是在写入过程中来读取,直到写入完全完成。因此,它的行为类似于方法上的同步。对吗?
  • @moamzia 它的行为就像一个 volatile 变量,这正是它的本质。如果volatile 允许读取弗兰肯值,那将完全没用。这里唯一的问题是你的错误假设。
  • 我认为这个答案非常令人困惑和误导。确实,如果不使用volatile,您可能会被撕裂,但您也可能会以其他方式失败。暗示单词撕裂是the 问题是一种误导和混淆。 volatile 不仅确保不会发生单词撕裂,它还针对任何可能的故障机制提供具体保证,即使在我们一无所知的硬件上也是如此。
  • x++ 这样的操作可以被设计成原子的,碰巧volatile 并没有这样做,因为volatile 的设计目的不是提供原子性,只是提供可见性和排序。在可见性暗示原子性的情况下,它“碰巧”提供原子性。但它总是提供可见性,这就是它的用途。按照措辞,答案表明volatile“在可能的情况下”提供了原子性,这是不正确的。
  • 没有人(可能除了 OP 之外)认为单词撕裂是问题,甚至是重要问题。但是,这个问题是专门询问longdouble ...和撕裂。确实,如果没有volatile(或其他形式的同步),您可能会感到撕裂。
【解决方案2】:

我不明白为什么在 double 或 long 上使用 volatile 会使其成为原子?

原因如下。将volatiledoublelong 一起使用会使它们原子化因为JLS 是这么说的

JLS (Section 17.7) 声明:

“volatile long 和 double 值的写入和读取始终是原子的。”


注意:JLS 是规范性的。就语言语义而言,javadocs 是非规范性的。 Bloch 的“Effective Java”甚至不是 Oracle 文档——它只是一个(未经授权的)评论。

【讨论】:

  • @moamzia 具体来说,Effective Java 不是规范性参考。
  • 它甚至不是 Oracle 文档。
  • 我自己也受够了,尤其是它被当作一对来自西奈山的石碑对待的方式。例如,请参阅here。谈论本末倒置。
  • 我相信发帖人实际上问为什么 volatile 使 i++ 成为原子。答案是它不会,因为它是一个复合语句。
  • @sola - 我不同意。再次阅读他的问题。他说他知道i++ 是非原子的,无论i 是否为volatile
猜你喜欢
  • 2019-12-27
  • 2015-12-28
  • 1970-01-01
  • 2011-03-03
  • 2011-06-19
  • 2013-07-10
  • 1970-01-01
  • 2013-01-18
  • 2012-10-19
相关资源
最近更新 更多