什么是“out-of-thin-air”安全?

9
我希望理解《Java并发实践》中的这一句话:“出于薄空气的安全性是指当一个线程在没有同步的情况下读取变量时,它可能会看到一个过期的值,但该值将是由一个线程设置的,而不是一些随机值。”这里的“随机值”是什么意思?它如何进入这种状态?提供伪代码示例将非常有帮助。

3
它无法处于那种状态。这就是你发布的片段所说的。你有一个保证,永远不会从一个值中读取随机值。它要么是当前的值,要么是旧的值,但不会是没有任何线程对该变量写入的值。 - JB Nizet
2
这只是说它保证是线程先前设置的值,它永远不会是一些未初始化的值,或者因为被抢占而部分设置的值。请参见https://dev59.com/UXvaa4cB1Zd3GeqPEYhQ,它解释了“有些架构将64位操作实现为可以中断的2个单独的32位操作”。 - Ruan Mendes
1
C和C ++是两门编程语言。 - Fred Larson
1
@Julian 我链接的答案没有展示一个具体的编程语言,但是它所描述的情况是指单词写入不是原子操作的一种语言? - Ruan Mendes
1
是的,Java可以处理64位值。这就是你引用代码片段后紧接着解释的内容(尽管你看到的值并不真正随机。它是仅设置该值的64位中的32位所得到的值)。 - JB Nizet
显示剩余3条评论
4个回答

5

现有的两个答案都描述了读写操作的撕裂,但在我看来这并不完全等同于虚构值

撕裂基本上意味着计算机重新写入数据。

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://dev59.com/UXvaa4cB1Zd3GeqPEYhQ#26500593)。

3
我能想到的最好例子是在32位CPU上写入long
根据Java标准,long必须是64位宽。32位CPU无法一次原子写入long值,必须写入两个32位整数才能实现。
假设一个线程只写了第一个32位值,然后操作系统调度了另一个(Java)线程在相同的核心上尝试读取那个半更新的值 - 它将看到随机值。你引用的话基本上意味着“不可能发生”。

1
更具体的例子可以是一个Person对象,它拥有address, citystate三个字段吗?此人可能有旧地址或新地址,但永远不会以部分更改地址的方式返回,从而导致不存在的地址。 - Compass
这是唯一一个简单明了且易于理解的答案。+1 - Hannes Schneidermayer

2
在Java中,变量在创建时会被初始化为“已知”值(默认值或提供的值,在int value = 5;情况下)。默认初始化在规范的第4.12.5节中描述。这意味着变量被初始化为对应类型的“零”值。通常,对变量的赋值是原子性的,即它们会立即发生,变量的值从一个值改变为新值。但对于longdouble类型的变量可能有例外。这在规范中有所描述。实际上,这些值是64位宽的,可以分两步设置,每个32位半部分设置一次。因此,变量的值可能处于以下状态之一:默认值(“零”/null),由线程设置的值,对于非易失性长整型或双精度浮点数,是期望值的一半(0|half 或 half|0)。总的来说,这些要求意味着变量的值不会出现莫名其妙的情况,不会读取随机数据(先前在该内存位置的任何数据),这与C或C++等语言不同。请注意保留HTML标签。

在Java中,变量在创建时会被初始化为默认值,但这并不适用于局部变量,只适用于成员变量。局部变量可以通过初始化器初始化为任何值。非常量成员变量即使有显式的初始化器,也会被初始化为它们的默认(“零值”)值。后者将使用初始化表达式的结果替换零值。 - Lew Bloch

1
“Out of thin air values” 是指由行动中的因果循环引起的价值观。
例如:
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的正式部分包含两个部分:”
“第一部分:一致性:每次读取都会看到:”
  • 要么是在happens before顺序中它之前的最近的写入
  • 要么是一个读/写操作,在该操作中读取和写入不按happens before顺序排序(需要处理数据竞争)。
“第二部分:因果关系”
“仅有一致性不能防止这种推测性的自我证明,因此需要至少有一个因果关系来解释执行。这个因果关系是一个偏序,并且因此禁止任何具有循环因果关系的执行,例如上面的例子。”
“出乎意料”与32位JVM上的64位长整型/双精度浮点类型无关。该问题称为读/写错误。
有关更多信息,请参见JSR-133 Java内存模型和线程规范第19页。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接