152

我在下面的某处读到。

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

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

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

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

4

6 回答 6

190

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

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

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

于 2013-11-02T17:08:38.430 回答
106

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

以 Java 中的两个线程为例:

线程 A:

value = 1;
done = true;

线程 B:

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

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

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

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

于 2013-11-02T18:20:11.133 回答
90

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

  1. 原子性
  2. 能见度

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

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

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

另一方面AtomicIntegerAtomicReference是基于比较和交换指令。CAS 具有三个操作数:V要操作的内存位置、预期的旧值A和新值BCAS自动更新V到新值B,但前提是 in 中的值V与预期的旧值匹配A;否则,它什么也不做。在任何一种情况下,它都会返回当前在 中的值V。如果底层处理器支持该功能的 compareAndSet()方法AtomicIntegerAtomicReference利用该功能的方法;如果不是,则 JVM 通过自旋锁实现它。

于 2013-11-02T17:22:02.350 回答
55

如前所述,volatile仅涉及可见性。

在并发环境中考虑这个片段:

boolean isStopped = false;
    :
    :

    while (!isStopped) {
        // do some kind of work
    }

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

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

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

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

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

于 2013-11-02T18:02:28.563 回答
22

使用volatile关键字:

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

根据java文档,这些java.util.concurrent.atomic.*类是:

一个小的类工具包,支持对单个变量进行无锁线程安全编程。本质上,此包中的类将 volatile 值、字段和数组元素的概念扩展到还提供以下形式的原子条件更新操作的类:

boolean compareAndSet(expectedValue, updateValue);

原子类是围绕compareAndSet(...)映射到原子 CPU 指令的原子函数构建的。原子类与变量一样引入了先发生顺序。volatile(有一个例外:)weakCompareAndSet(...)

从java文档:

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

对于你的问题:

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

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

AtomicInteger和 和有什么不一样volatile int

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

于 2013-11-02T18:22:21.153 回答
16

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

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

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

没有锁。这就是synchronized目的。

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

是的,

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

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

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

于 2013-11-02T18:17:04.017 回答