当前位置: 首页 > 知识库问答 >
问题:

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

卢阳成
2023-03-14

在此处的oracle Java文档中,说明如下:

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

它还说:

  • 对于引用变量和大多数基本变量(除long和double之外的所有类型),读写都是原子的
  • 对于所有声明为volatile的变量(包括long和double变量),读写都是原子的

我对这些发言有两个问题:

>

如果说在非双精度、非长原语上放置易失性的唯一效果是启用与其他线程的后续读取的“先发生”关系,这是真的吗?我问这个问题是因为这些变量似乎已经有了原子读取。

共有3个答案

齐思淼
2023-03-14

对于问题1,风险仅被降低(而不是消除),因为易失性仅适用于单个读/写操作,而不适用于更复杂的操作,例如增量、减量等。

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

商宝
2023-03-14

常见示例:

while(!condition)
    sleep(10);

如果条件不稳定,则其行为与预期相同。如果不是,则允许编译器将其优化

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

这与原子性完全正交:如果条件是假设的非原子整数类型,则序列

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将变量读取为零。在这种情况下,Volity关键字确保线程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强制执行必要的排序。

许照
2023-03-14

他们所说的“降低风险”是什么意思?

原子性是Java内存模型解决的一个问题。然而,比原子性更重要的是以下问题:

  • 内存体系结构,例如CPU缓存对读写操作的影响

下面的列表包含一个常用的示例。x和y上的操作是原子操作。尽管如此,程序仍可以打印这两行。

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");

但是,如果将x和y声明为volatile,则只能打印这两行中的一行。

使用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);

只有在发生之前,两个0s才是有效的结果。然而,这显然是违反直觉的。因此,Java提供了一个更严格的一致性模型,禁止这种相对性问题,这就是所谓的顺序一致性。您可以在Java语言规范的第17.4.3节和第17.4.5节中找到它。最重要的部分是:

当且仅当所有顺序一致的执行都没有数据争用时,程序才正确同步。如果一个程序正确同步,则该程序的所有执行似乎顺序一致(§17.4.3)。

这意味着,volatile给您带来的比以前更多。如果用于所有冲突的访问(§17.4.3),它可以提供顺序一致性

 类似资料:
  • 问题内容: 如果高速缓存一致性是在硬件级别实现的,为什么我们需要可变的?任何内核/处理器都应该获得最新值吗? 还是完全解决了另一个问题? 问题答案: 高速缓存一致性可以在处理器级别实现,但是,除非处理器内存模型保证顺序一致性(在大多数现代体系结构中不是这种情况),否则只有在需要时才会获得高速缓存一致性。 这就是volatile的含义:它要求JVM生成相关的机器指令,这些指令将要求处理器将其缓存与主

  • 如果缓存一致性是在硬件级别实现的,为什么我们需要volatile?任何核心/处理器都应该获得最新的值? 还是它完全在处理一个不同的问题?

  • 我正在看java jpoint会议的视频。 我对以下Alexey Shipilev报告幻灯片有疑问: 请原谅幻灯片上的非英语。实际上作者说变量集是不可能的 附注。 如果我理解Alexey符号正确,它尊重以下代码:

  • 在阅读了这个问题和这个(尤其是第二个答案)之后,我对volatile及其关于记忆障碍的语义感到非常困惑。 在上面的例子中,我们写入一个易失性变量,这会导致一个mitch,这反过来会将所有挂起的存储缓冲区/加载缓冲区刷新到主缓存,使其他缓存行无效。 然而,非易失性字段可以优化并存储在寄存器中,例如?那么,我们如何才能确保给定一个写入易失性变量之前的所有状态变化都是可见的呢?如果我们有1000件东西呢

  • 我对易变语义几乎没有疑问 假设有三个线程T1、T2和T3,以及给定类的一个实例。 假设发生以下读/写操作序列: 我知道可以保证点9的T1将看到点7的值,点10的T1将看到点6的值(确切地说,至少和这个值一样最新)。 但是,这些说法是真的吗? Java内存模型保证,点11处的T1将看到至少与点5处的T3相同的最新值(来自T3或更实际的本地内存的值,但即使共享内存中有更实际的值,T1也可能看不到) 请

  • 我想澄清发生前关系是如何与不稳定变量一起工作的。让我们有以下变量: 和线程A: 和线程B: 根据Java内存模型(JMM),以下语句是否正确?如果不是,正确的解释是什么? 总是发生-之前 在JMM中发生在之前,只有当它实际发生在时间之前 在JMM中发生在之前(并且将被可预测地分配)如果实际发生在