易失性关键字和内存一致性错误

时间:2014-06-30 14:09:05

标签: java multithreading volatile

在oracle Java文档located here中,提到了以下内容:

  

原子操作不能交错,因此可以使用它们而不用担心线程干扰。但是,这并不能消除所有同步原子操作的需要,因为仍然可能存在内存一致性错误。使用volatile变量可降低内存一致性错误的风险,因为对volatile变量的任何写入都会建立与之后读取同一变量的先发生关系。这意味着对volatile变量的更改始终对其他线程可见。更重要的是,它还意味着当线程读取volatile变量时,它不仅会看到volatile的最新更改,还会看到导致更改的代码的副作用。

它还说:

  • 读取和写入对于参考变量和大多数都是原子的 原始变量(除了long和double之外的所有类型)。
  • 读取和写入对于声明为volatile的所有变量都是原子的(包括long 和双变量)。

我对这些陈述有两个问题:

  1. “使用volatile变量可以降低内存一致性错误的风险” - “降低风险”是什么意思,使用volatile时内存一致性错误怎么样?

  2. 将volatile放在非双非长基元上的唯一效果是,是否可以启用与其他线程后续读取的“先发生”关系?我问这个,因为这些变量似乎已经有原子读数。

6 个答案:

答案 0 :(得分:3)

“降低风险”是什么意思?

Atomicity Java内存模型解决的一个问题。但是,比 Atomicity 更重要的是以下问题:

  • 内存架构,例如CPU缓存对读写操作的影响
  • CPU优化,例如重新装载货物和商店
  • 编译器优化,例如添加和删​​除装载和存储

以下列表包含一个经常使用的示例。 xy上的操作是原子的。不过,该程序可以打印两行。

int x = 0, y = 0;

// thread 1
x = 1
if (y == 0) System.out.println("foo");

// thread 2
y = 1
if (x == 0) System.out.println("bar");

但是,如果您将xy声明为volatile,则只能打印两行中的一行。


使用volatile时,内存一致性错误仍然存​​在?

以下示例使用volatile。但是,更新可能仍会丢失。

int x = 0;

// thread 1
x += 1;

// thread 2
x += 1;

将volatile放在非双非长基元上的唯一效果是,是否可以启用与其他线程后续读取的“先发生”关系?

发生之前经常被误解。 定义的一致性模型发生在之前很弱并且难以正确使用。这可以通过以下示例进行演示,该示例称为独立写入的独立读取(IRIW):

volatile int x = 0, y = 0;

// thread 1
x = 1;

// thread 2
y = 1;

// thread 3
if (x == 1) System.out.println(y);

// thread 4
if (y == 1) System.out.println(x);

只有发生在之前,两个0才会生效。然而,这显然是违反直觉的。因此,Java提供了更严格的一致性模型,禁止此相对性问题,这被称为sequential consistency。您可以在 Java语言规范§17.4.3§17.4.5部分找到它。最重要的部分是:

  

当且仅当所有顺序一致的执行没有数据争用时,程序才能正确同步。如果程序正确同步,则程序的所有执行都将显示为顺序一致(第17.4.3节)。

这意味着,volatile为您提供的不仅仅是发生在之前。如果用于所有冲突访问§17.4.3),它会为您提供顺序一致性

答案 1 :(得分:1)

通常的例子:

while(!condition)
    sleep(10);

如果condition是易失性的,则表现如预期。如果不是,则允许编译器将其优化为

if(!condition)
    for(;;)
        sleep(10);

这与原子性完全正交:如果condition是一个非原子的假设整数类型,那么序列

thread 1 writes upper half to 0
thread 2 reads upper half (0)
thread 2 reads lower half (0)
thread 1 writes lower half (1)

当变量从非零值更新时可能发生,该非零值恰好具有零的下半部分到具有零的上半部分的非零值;在这种情况下,线程2将变量读取为零。在这种情况下,volatile关键字确保线程2确实读取变量而不是使用其本地副本,但它不会影响计时。

第三,原子性不能防止

thread 1 reads value (0)
thread 2 reads value (0)
thread 1 writes incremented value (1)
thread 2 writes incremented value (1)

使用原子易变变量的最佳方法之一是环形缓冲区的读写计数器:

thread 1 looks at read pointer, calculates free space
thread 1 fills free space with data
thread 1 updates write pointer (which is `volatile`, so the side effects of filling the free space are also committed before)
thread 2 looks at write pointer, calculates amount of data received
...

这里,不需要锁定来同步线程,原子性保证始终一致地访问读写指针,volatile强制执行必要的排序。

答案 2 :(得分:1)

对于问题1,风险只会降低(而不是消除),因为volatile仅适用于单个读/写操作,而不适用于更复杂的操作,如递增,递减等。

对于问题2,volatile的作用是使更改立即对其他线程可见。正如引用的段落所述“这并不能消除所有同步原子动作的需要,因为内存一致性错误仍然存​​在。”仅仅因为读取是原子的并不意味着它们是线程安全的。因此,在关系之前建立一个发生几乎是保证跨线程内存一致性的(必要的)副作用。

答案 3 :(得分:0)

Ad 1:对于volatile变量,始终根据主副本检查变量,并且所有线程都看到一致的状态。但是如果你在非原子操作中使用该波动率变量写回结果(比如a = f(a))那么你可能仍然会产生内存不一致。这就是我理解这句话“降低风险”的方式。易失性变量在读取时是一致的,但您仍可能需要使用同步。

广告2:我不知道。但是:如果你的“之前发生”的定义包括评论

  

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

我不敢依赖任何其他财产,除了挥发性确保这一点。你对它还有什么期望?!

答案 4 :(得分:0)

假设您有一个带CPU缓存或CPU寄存器的CPU。在核心数量方面独立于CPU架构,volatile不能保证完美的不一致性。实现此目的的唯一方法是使用具有性能价格的同步或原子引用。

例如,您有多个线程(线程A和线程B)处理共享数据。假设线程A想要更新共享数据并且它已经启动。出于性能原因,线程A的堆栈被移动到CPU缓存或寄存器。然后,线程A更新了共享数据。但是这些地方的问题是实际上他们没有立即将更新后的值刷回主存储器。这是提供不一致的地方,因为直到闪回操作,线程B可能想要使用相同的数据,这可能是从主内存中获取的 - 但是没有更新的值。

如果使用volatile,则所有操作都将在主内存上执行,因此您没有刷新后退延迟。但是,这次你可能会遇到线程管道。在写操作的中间(由多个原子操作组成),操作系统可能已经执行了线程B来执行读操作,就是这样!线程B将再次读取未更新的值。这就是它说降低风险的原因。

希望你明白了。

答案 5 :(得分:-1)

进入并发时,您可能需要确保两件事:

  • 原子操作:一组操作是原子的 - 这通常是通过实现的 “同步”(更高级别的结构)。还有volatile,例如long和double的读/写。

  • 可见性:线程B看到线程A做出的修改。即使操作是原子操作,如写入int变量,第二个线程仍然可以看到非最新值变量,由于处理器缓存。将变量设置为volatile可确保第二个线程确实看到该变量的最新值。更重要的是,它确保第二个线程在写入volatile变量之前看到第一个线程写入的所有变量的最新值。