【问题标题】:What is out-of-thin-air safety?什么是无中生有的安全?
【发布时间】:2017-03-03 20:40:04
【问题描述】:

我想从 Java Concurrency in Practice 中理解这一行 -

无中生有的安全性是一种安全保证,即当一个线程在没有同步的情况下读取一个变量时,它可能会看到一个陈旧的值,但该值将是一个线程设置的值,而不是某个随机值。

随机值是什么意思?怎么会进入那种状态?一个伪代码示例会很有帮助。

【问题讨论】:

  • 它无法进入那种状态。这就是您发布的sn-p所说的。您可以保证永远不会从值中读取随机值。它要么是当前的,要么是旧的,但不是没有线程曾经写入变量的值。
  • 这只是说它保证是一个线程先前设置的值,它永远不会是一些未初始化的值,或者因为它被抢占而设置了一半的值。请参阅stackoverflow.com/questions/21243858/out-of-thin-air-safety,它解释说“有些架构将 64 位操作实现为 2 个可以中断的独立 32 位操作”
  • C 和 C++ 是两个。
  • @Julian 我链接的答案没有显示示例语言,但它会是单词写入不是原子的?
  • 是的,Java,用于 64 位值。那是在您引用的 sn-p 之后立即解释的(尽管您可能看到的值并不是真正随机的。它是通过仅设置 64 位值中的 32 位获得的值)。

标签: java multithreading concurrency


【解决方案1】:

现有的两个答案都描述了读取和写入的撕裂,在我看来,这与 out-of-thin-air 值并不完全相同。

撕裂基本意思是电脑改写

int g = 0x1234;
void threadA() {
    g = 0xABCD;
}
void threadB() {
    int local = g;
}

进入

int g = 0x1234;
void threadA() {
    g = 0xAB00;  // "tearing"
    g += 0x00CD;
}
void threadB() {
    int local = g;  // may read 0xAB00, which threadA never stored
}

(根据指令集,您可能会想象看到 0xAB34、0x12CD、0x00CD、0x00AB 等)


然而,out-of-thin-air(以我有限的经验)通常意味着编译器会重写

int g1 = 0x1234;
bool g2 = true;
void threadA() {
    g1 = (g2 ? 0xABCD : 0x5678);  // since g2 is true, this stores 0xABCD
}
void threadB() {
    int local = g1;
}

进入

int g1 = 0x1234;
bool g2 = false;
void threadA() {
    g1 = 0x5678;
    if (g2) g1 = 0xABCD;
}
void threadB() {
    int local = g1;  // may read 0x5678, which threadA never stored
}

这与“推测执行”(我认为它更像是一个硬件术语)密切相关,并且它确实依赖于读取或写入的撕裂。

另请参阅我对这个重复问题的回答 (https://stackoverflow.com/a/26500593/1424877)。

【讨论】:

    【解决方案2】:

    我能想到的最好的例子是在 32 位 CPU 中编写 long

    根据 Java 标准,long 必须是 64 位宽。 32 位 CPU 无法在一次原子写入中写入 long 值,它必须写入两个 32 位整数才能实现。

    假设一个线程只写入了前 32 位值,那么,操作系统在同一个内核上调度另一个 (Java) 线程来尝试读取该半更新值 - 它会看到随机值。你给出的报价基本上是说“这不可能发生”。

    【讨论】:

    • 更具体的例子是具有addresscitystate 字段的Person 对象吗?该人可能有旧地址或新地址,但绝不会以仅部分更改地址的方式返回,从而导致地址不存在。
    【解决方案3】:

    在 Java 中,变量在创建时使用“已知”值进行初始化(在int value = 5; 情况下,默认值、“类似零”值或提供的值)。

    spec 的 4.12.5 节描述了默认初始化。

    这意味着变量被初始化为有效地“零”(如对该类型的解释)。

    现在,通常对变量的赋值是原子的。也就是说,它们“同时”发生,变量的值从一个值变为新值。

    longdouble 类型的变量可能会出现例外情况。这在spec 中有描述。 实际上,这些值是 64 位宽,可以分两步设置,每个 32 位一半。

    综上所述,上述要求意味着变量的值可能处于以下状态之一:

    1. 默认值(“零”/null)
    2. 线程设置的值
    3. 对于非易失性 long 或 double,为所需值的一半(0|half 或 half|0)

    相当确定,并且可以说,没有任何值可以凭空读取。

    这与不总是初始化变量的 C 或 C++ 等语言形成对比。因此,您可以读取一些随机数据(该内存位置之前的任何数据)。

    【讨论】:

    • “在 Java 中,一个变量在创建时使用默认值初始化”对于局部变量不正确,仅对于成员变量。局部变量可以初始化为给定初始化器的任何值。即使使用显式初始化程序,非常量成员变量也会使用其默认(“类似零”)值进行初始化。后者用初始化表达式的结果替换类似零的值。
    【解决方案4】:

    无中生有的价值是由行动中的因果循环引起的价值。

    例如

    int a=0
    int b=0
    
    Thread1:
       r1=a (1)
       b=r1 (2)
    
    Thread2:
       r2=b (3)
       a=r2 (4)
    

    我们最终会得到 r1=42 和 r2=42 吗?这听起来很疯狂,因为值 42 从未被写入,所以我们如何才能看到这些值。

    假设运行线程 2 的 CPU 会推测 (3) 处的 b=42。这将导致 r2=42 和 a=42 (4)。这将导致 r1=42 (1) 和 b=42 (2)。运行线程 2 的 CPU 现在非常高兴,因为推测已经奏效,因为它推测的值是读取的实际值!

    JVM 不允许这种自我证明。 JMM 的正式部分包含 2 个部分:

    第一部分:一致性:每次读取都会看到:

    • 在发生之前的顺序中之前的最新写入
    • 或写入,其中读取/写入未按之前发生的顺序排序(需要处理数据竞争)。

    第二部分:因果关系

    仅靠一致性并不能防止这种推测性的自我辩护,因此需要至少有 1 个因果顺序来解释执行。这种因果顺序是部分顺序,因此禁止任何在因果顺序中具有循环的执行,如上述。

    凭空而来的与 32 位 JVM 上的 64 位长/双精度无关。这个问题被称为撕裂的读/写。

    有关更多信息,请参阅JSR-133 Java Memory Model and Thread Specification 第 19 页。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-03-28
      • 2013-11-25
      • 2020-05-20
      • 2019-01-08
      • 1970-01-01
      • 2019-05-06
      相关资源
      最近更新 更多