【问题标题】:Volatile Vs Atomic [duplicate]易失性与原子性[重复]
【发布时间】:2013-11-13 16:49:59
【问题描述】:

我在下面的某处读到。

Java volatile 关键字并不意味着原子,它的常见误解 在声明 volatile 之后,++ 操作将是原子的,使 您仍然需要使用原子操作来确保独占访问 synchronized Java 中的方法或块。

那么如果两个线程同时攻击volatile 原始变量会发生什么?

这是否意味着无论谁锁定它,都会首先设置它的值。如果与此同时,其他线程出现并读取旧值,而第一个线程正在更改其值,那么新线程不会读取其旧值吗?

Atomic 和 volatile 关键字有什么区别?

【问题讨论】:

  • AtomicInteger 和 volatile int 有什么区别?
  • 原子操作,如++。那里有大约 10 个原子操作
  • SHORT ANSWERvolatile 使 ONLY ONE 原子读取 OR 写入,原子可以执行很少的原子操作,例如获取添加

标签: java volatile


【解决方案1】:

volatile 关键字的效果大致是使对该变量的每个单独的读取或写入操作对所有线程都以原子方式可见。

然而,值得注意的是,需要多次读/写的操作——例如i++,相当于i = i + 1,它执行一次读和一次写——不是 原子,因为另一个线程可能会在读取和写入之间写入i

Atomic 类,如 AtomicIntegerAtomicReference,以原子方式提供更广泛的操作,特别是包括 AtomicInteger 的增量。

【讨论】:

  • 补充一点,AtomicInteger中存储的intprivate volatile int value;
  • @Vallentin:是的,但是AtomicInteger 使用一些特殊的实用程序——特别是包括Unsafe.compareAndSwapInt——来安全地执行更强大的并发操作。
  • 这只是事实的一半,您错过了非常重要的执行顺序问题。请参阅下面的解释。
  • @LouisWasserman:“volatile 关键字的效果是......对该变量的操作是原子的。”我必须不同意这种说法的真实性。对 Java 中非 8 字节变量的单独读取和写入始终是原子的。 volatile 关键字不会对本来不是原子的变量进行访问。它只是强制处理环境立即将对变量所做的更改刷新到主内存,以便所有线程都可以看到这些更改。
  • @ortang:只是一个肯定。我检查了 JLS (s17.7)。您是正确的,volatile 声明除了提供线程间可见性之外,还将对 longs 和 doubles(否则将不是原子的)进行读取和写入原子访问。
【解决方案2】:

多线程环境中有两个重要的概念:

  1. atomicity
  2. visibility

volatile 关键字消除了可见性问题,但它不处理原子性。 volatile 将阻止编译器重新排序涉及 volatile 变量的写入和后续读取的指令;例如k++。 这里k++不是一条机器指令,而是三条:

  1. 将值复制到寄存器;
  2. 增加值;
  3. 放回原处。

因此,即使您将变量声明为volatile,这也不会使该操作成为原子操作;这意味着另一个线程可以看到一个中间结果,该结果对于另一个线程来说是陈旧的或不需要的值。

另一方面,AtomicIntegerAtomicReference 是基于Compare and swap instruction。 CAS 具有三个操作数:要对其进行操作的内存位置V、预期的旧值A 和新值BCAS 原子地将V 更新为新值B,但前提是V 中的值与预期的旧值A 匹配;否则,它什么也不做。无论哪种情况,它都会返回当前在V 中的值。如果底层处理器支持,AtomicIntegerAtomicReferencecompareAndSet() 方法会利用此功能;如果不是,则 JVM 通过spin lock 实现它。

【讨论】:

    【解决方案3】:

    如前所述,volatile 仅处理可见性。

    在并发环境中考虑这个 sn-p:

    boolean isStopped = false;
        :
        :
    
        while (!isStopped) {
            // do some kind of work
        }
    

    这里的想法是,某些线程可以将isStopped 的值从 false 更改为 true,以便向后续循环指示是时候停止循环了。

    直觉上是没有问题的。从逻辑上讲,如果另一个线程使isStopped 等于真,那么循环必须终止。现实情况是,即使另一个线程使 isStopped 等于 true,循环也可能永远不会终止。

    这样做的原因并不直观,但请考虑现代处理器具有多个内核,并且每个内核都有多个寄存器和多级高速缓存其他处理器无法访问。换句话说,缓存在一个处理器的本地内存中的值对于在不同处理器上执行的线程不可见。这就是并发的核心问题之一:可见性。

    Java Memory Model 不保证对一个线程中的变量所做的更改何时对其他线程可见。为确保更新一经生成即可见,您必须进行同步。

    volatile 关键字是一种弱同步形式。虽然它对互斥或原子性没有任何作用,但它确实提供了一种保证,即在一个线程中对变量所做的更改将在它发生后立即对其他线程可见。由于对非 8 字节变量的单独读写在 Java 中是原子性的,因此声明变量 volatile 提供了一种简单的机制,可以在没有其他原子性或互斥要求的情况下提供可见性。

    【讨论】:

    • 谢谢,我认为这个答案对我来说是一个更好的答案。很清楚。我从一个原始布尔变量中停止了一个带有简单状态值的线程,我想知道为什么它没有停止某个运行点(有时它确实停止了)...,
    【解决方案4】:

    易失性和原子性是两个不同的概念。 Volatile 确保某个预期的(内存)状态在不同线程中为 true,而 Atomics 确保对变量的操作以原子方式执行。

    以Java中的两个线程为例:

    线程 A:

    value = 1;
    done = true;
    

    线程 B:

    if (done)
      System.out.println(value);
    

    value = 0done = false 开始,线程规则告诉我们,线程B 是否打印值是未定义的。 此外,value 在那一点上也是未定义的! 要解释这一点,您需要了解一点 Java 内存管理(可能很复杂),简而言之:线程可能会创建变量的本地副本,并且 JVM 可以重新排序代码以优化它,因此不能保证上述代码完全按照该顺序运行。将 done 设置为 true 并且 then 将值设置为 1 可能是 JIT 优化的结果。

    volatile 仅确保在访问此类变量时,新值将立即对所有其他线程可见并且执行顺序确保代码位于您期望的状态。所以在上面代码的情况下,将done定义为volatile将确保每当线程B检查变量时,它要么是假的,要么是真的,如果是真的,那么value有也被设置为 1。

    作为 volatile 的副作用,这种变量的值是在线程范围内以原子方式设置的(以非常小的执行速度成本)。然而,这仅在 32 位系统上很重要,即使用长(64 位)变量(或类似变量),在大多数其他情况下,设置/读取变量无论如何都是原子的。但是原子访问和原子操作之间有一个重要的区别。 Volatile 只保证访问是原子的,而 Atomics 保证操作是原子的。

    举个例子:

    i = i + 1;
    

    无论你如何定义 i,在执行上述行时读取值的不同线程可能会得到 i 或 i + 1,因为 操作 不是原子的。如果另一个线程将 i 设置为不同的值,在最坏的情况下,线程 A 可以将 i 设置回它之前的任何值,因为它只是在根据旧值计算 i + 1 的中间,然后设置 i再次回到那个旧值 + 1。解释:

    Assume i = 0
    Thread A reads i, calculates i+1, which is 1
    Thread B sets i to 1000 and returns
    Thread A now sets i to the result of the operation, which is i = 1
    

    像 AtomicInteger 这样的原子确保此类操作以原子方式发生。所以上述问题不会发生,一旦两个线程都完成,我将是 1000 或 1001。

    【讨论】:

    • "将 done 设置为 true,然后将 value 设置为 1 可能是 JIT 的结果。" - 什么?你一定是在抽烟吧?这听起来不正确。你确定吗?这真的可能吗?
    • 在这种特殊情况下是可能的,因为从线程的本地角度来看,这样做没有问题。所以如果对 CPU 方便(即内存的批量写入),以这种方式重新排序是很有可能的。
    • @momomo 这正是Java memory model spec 警告的内容,请参阅第一个示例
    • 那么,如果我有一个不符合volatile 条件的 AtomicBoolean,是否有可能另外 2 个线程同时获得不同的值?
    • 没关系,我在这里得到了答案,AtomicBoolean 已经隐含地被volatile 限定了。 stackoverflow.com/a/42868221/2218838
    【解决方案5】:

    使用了volatile关键字:

    • 使非原子 64 位操作原子化:longdouble。 (所有其他原始访问已经保证是原子的!)
    • 保证变量更新可以被其他线程看到 + 可见性效果:写入 volatile 变量后,在写入该变量之前可见的所有变量在读取相同的 volatile 变量后对另一个线程可见(happen-before订购)。

    java.util.concurrent.atomic.* 类是,根据java docs

    支持无锁线程安全的类的小工具包 对单个变量进行编程。本质上,这个类 包扩展了 volatile 值、字段和数组的概念 那些也提供原子条件更新的元素 表单的操作:

    boolean compareAndSet(expectedValue, updateValue);

    原子类是围绕映射到原子 CPU 指令的原子 compareAndSet(...) 函数构建的。原子类引入了 happen-before 排序,就像 volatile 变量所做的那样。 (有一个例外:weakCompareAndSet(...))。

    来自 java 文档:

    当一个线程看到一个原子变量的更新是由一个 weakCompareAndSet,它不一定看到任何其他的更新 在weakCompareAndSet 之前发生的变量。

    你的问题:

    这是否意味着无论谁锁定它,都将设置 首先是它的价值。与此同时,出现了一些其他的线索 在第一个线程更改其值时读取旧值,然后不 新线程会读取它的旧值吗?

    您没有锁定任何东西,您所描述的是一种典型的竞争条件,如果线程在没有适当同步的情况下访问共享数据,最终会发生这种情况。如前所述,在这种情况下声明变量 volatile 只会确保其他线程将看到变量的更改(该值不会缓存在某些缓存的寄存器中,只有一个线程才能看到)。

    AtomicIntegervolatile int 有什么区别?

    AtomicIntegerint 上提供原子操作并具有适当的同步(例如incrementAndGet()getAndAdd(...)、...),volatile int 将确保int 对其他线程的可见性。

    【讨论】:

    • atomic 和 volatile 是两个相互排斥的概念。见stackoverflow.com/a/19744659/632951
    • Java 规范 17.7:...a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write. Writes and reads of volatile long and double values are always atomic. Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values. 链接:docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7
    • @Pacerier 我大体上同意你的观点,但在 java 中有一个问题,你需要 volatile 关键字来使 64 位值原子化。因此它们不再相互排斥。
    【解决方案6】:

    那么如果两个线程同时攻击一个 volatile 原始变量会发生什么?

    通常每个都可以增加值。但是有时,两者都会同时更新值,而不是总共增加 2,而是两个线程都增加 1,并且只添加 1。

    这是否意味着无论谁锁定它,都会首先设置它的值。

    没有锁。这就是synchronized 的用途。

    如果与此同时,其他线程出现并读取旧值,而第一个线程正在更改其值,那么新线程不会读取其旧值吗?

    是的,

    Atomic 和 volatile 关键字有什么区别?

    AtomicXxxx 封装了一个 volatile,所以它们基本相同,不同的是它提供了更高级别的操作,例如用于实现增量的 CompareAndSwap。

    AtomicXxxx 也支持lazySet。这就像一个 volatile 集,但不会停止等待写入完成的管道。这可能意味着如果您读取一个刚刚写入的值,您可能会看到旧值,但无论如何您都不应该这样做。不同的是,设置一个 volatile 大约需要 5 ns,bitlazySet 大约需要 0.5 ns。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-04-04
      • 1970-01-01
      • 1970-01-01
      • 2010-09-14
      • 1970-01-01
      • 2018-08-09
      • 1970-01-01
      • 2023-03-21
      相关资源
      最近更新 更多