我在本网站上发现了一些有趣的问题(例如this one),这些问题是关于源自本书Java concurrency in Practice的本段的Java中易变变量的可见性影响:
volatile变量的可见性效果超出了volatile变量本身的值。当线程A写入易失性变量并且随后线程B读取相同的变量时,在写入易失性变量之前,A可见的所有变量的值在读取volatile变量后变为B可见。因此,从内存可见性的角度来看,编写volatile变量就像退出synchronized块一样,读取volatile变量就像进入synchronized块一样。
然而,即使在阅读了本网站相关问题的答案之后,有一种情况对我来说并不完全清楚,具体而言:
线程 A0 在线程 A 之前写入相同的易变变量 会产生什么影响?
换句话说: A0 写入volatile变量,此值稍后被 A (不读取变量)覆盖,然后读取乙。 (所以我们有两个来自不同线程的写操作( A0 和 A )和来自第三个线程( B )的读操作。 / p>
我可以安全地假设 A 和 B 都可以看到 A0 可见的一切在 A0 写入volatile变量之前? 更新 这是关于Java内存模型的概念性问题。
我知道我无法预测在线程 A0 和 A 以及读入线程 B 中发生对volatile变量的写入的顺序。
但是,为了便于讨论,我们可以这样说
A0 在 A 之前启动了很多时间,并且在 B 启动了另一个大量时间之后,让我们做出简化的假设足以保证写入和读取按所描述的顺序发生(我知道订单不能仅通过时序来保证,这只是为了避免偏离原始问题而进行的简化)。
答案 0 :(得分:3)
我可以安全地假设A和B都能保证在A0写入volatile变量之前看到A0可见的所有内容吗?
写一个易失性函数并不给出一个线程任何发生之前保证有关读取的内容。
当B
读取时,只要它看到它所做的更新,它就会看到A0可以看到的任何内容。如果B
看到值A
,A
也可以看到B
看到的任何内容。否则A0
可能会看到比任何一个线程更旧的状态,如果它读取的值不是来自该线程,即为时太早。
如果A
尝试先写入,则可能会在A
之后完成,例如说A0
与数据的干净副本位于同一个套接字上。它可以在{{1}}之前访问它,因为后者将花费更长的时间来获取缓存行。它是最后完成的,它决定了数据的最终状态。
答案 1 :(得分:3)
我认为情景非常简单。
A0 - 写入易变量 V 值 WA0
A - 写入易变量 V 值 WA
现在
我可以安全地假设A和B都能保证看到一切 在A0写入volatile变量之前A0可见?
如果线程A仅写入 V ,则在写入V之前,它可能会也可能看不到A0可见的所有内容。
仅当线程A读取变量 V 并且它读取的值变为WA0时,只有在线程A保证在写入V之前看到A0可见的所有内容强>
线程B也是如此,它取决于在读取V的值后B看到的值。
如果B读取V的值为WA,那么它将看到在线程A中写入V之前发生的所有事情。
如果B将V的值读取为WA0,那么它将在线程A0中写入V之前看到所有发生的事情。
另请注意
Thread A0
a = 1; // non volatile write
V = WA0; // volatile write
Thread A;
a=3
V = WA; // volatile write
Thread B;
while(V == 'WA') {
assert(a,3); // This may fail
}
您需要了解线程b中的代码未正确同步 volatile提供与可见性和重新排序相关的保证,但不具有原子性。
因此,即使线程B读取V的值为'WA'并且保证在写入V之前看到线程A中发生的所有事情,它并不一定意味着它将看到a的值为3,因为在读取V的值之后很可能发生了这样的事情,因为WA线程A0写入 a为1 ,这变为可用于线程B,从而使断言失败。发生之前保证在写入v之前必须发生的所有事情已经发生但它并不意味着你看不到未来的值。 您可以通过执行类似的操作轻松地重现场景
Thread B;
while(V == 'WA') {
Thread.sleep(1000);
assert(a,3); // This may fail
}
因此,这些场景单个Writer更受欢迎,或者您的程序不应该在线程B中具有上述代码,因为那样您的程序中就会出现数据争用。
编辑:
修改示例:
Thread 1 (T1)
a = 1; // normal write
b = 1; // normal write
v = 1; // volatile write
Thread 2 (T2)
a = 2; // normal write
c = 2; // normal write
v = 2; // volatile write
Thread 3 (T3)
while(true) {
if(v == 2) {
assert (c == 2); // will pass
assert (b == 1); // may fail if T1 hasn't run till
assert (a == 2); // may fail if T1 has run and set the value to 1
break;
}
if(v == 1) {
assert (b == 1); // will pass
assert (c == 2); // may fail if T2 hasn't run till
assert (a == 1); // may fail if T2 has now run setting a == 2
break;
}
}
“ 为了便于讨论,让我们说A0 在A做之前开始很多时间,并且在另一个重要之后开始 B开始的时间量。 “作为OP状态
现在,如果我重申上述陈述(v == 1发生之前 ( hb )v == 2)。
即我假设我保证v == 1 hb (v == 2)
然后,线程3的行为将更改为
线程3(T3)
while(true) {
if(v == 2) {
assert (c == 2); // will pass
assert (b == 1); // will pass
assert (a == 2); // will pass
break;
}
if(v == 1) {
assert (b == 1); // will pass
assert (c == 2); // may fail if T2 hasn't run till
assert (a == 1); // may fail if T2 has now run setting a == 2
break;
}
}
上述行为的问题是你如何保证
v == 1 hb(v == 2)
我想如果你能理解如何建立上述保证,那么你自己就可以回答你的问题了。
一种方法是建立@ishrat在线程A中的方式。 jmm中还有其他方法。
但单靠时间无法实现这种保证,您需要依赖于您正在使用的语言规范和平台的基础保证。
另请阅读此优秀的article about jmm
答案 2 :(得分:0)
对易失性变量v(第8.3.1.4节)的写入与任何线程对v的所有后续读取同步(其中“后续”根据同步顺序定义)。 JLS 17.4.4
因此,来自A0 的写入与来自B的读取同步所有来自A0 的早期写入发生在以下从B读取之前。如果没有其他写入这些变量,然后是,B必须看到它们。
我找不到A与A0同步。所以,不能保证A的可见性。
从更新的问题,我们有以下执行,其中A0,A1和B是线程,t0,t1,t2是语句和writeV,readV是写入/读取到易失性V:
A0 A1 B
t0 <-- writeV
^ t1 <-- writeV
| ^
| \------readV <-- t2
\-----------------------/
箭头表示发生在之前。我们有t0和t1 发生在 t2之前。 t2将从t0和t1看到更新。
因此,是的:B保证在t0中看到A0写的所有内容。它将读取A写的易失性值。
但不 t0 在之前发生,反之亦然。 因此,不:A不能保证看到A0写的所有内容。
然而,这只是概念性的,在现实生活中B无法知道A0是否被执行。
答案 3 :(得分:0)
我可以安全地假设A和B都能保证在A0写入volatile变量之前看到A0可见的所有内容吗?
否即可。对于易失性存储,当涉及谓词时,会在之前生成边缘。例如,为了让线程A
能够看到A0
写入所做的所有写操作,它需要在volatile写入时进行断言。
int a,b;
volatile c;
A0:
a = 10;
b = 5;
c = 20;
A:
if(c == 20){
// here all writes by A0 are guaranteed to be visible (ie: a and b).
c = 50;
}
如果我们引入第三个线程B
,则需要进行类似的检查:
int a,b;
volatile c;
A0:
a = 10;
b = 5;
c = 20;
A:
if(c == 20){
c = 50;
}
B:
if(c == 50){
// B will see all writes done by A and also A0.
}
因此,您正在寻找的保证与写入和后续读取定义的这些发生之前的边缘相关联。
否则,主题A和B可以将a
,b
和c
读作默认值0或写入的值。
然而,为了便于讨论,我们可以说A0在A开始之前开始很多时间,并且在B开始另一个大量时间后开始。
在实践中,你可能不会遇到内存问题,但是你不应该提前用这个想法编写你的软件。
另外,我怀疑你的意思是这样,但如果你的意思是 start - 一个线程的写入发生在另一个线程的启动之前(开始)发生在同一个线程中。)
如果我们从A
删除了读取但保留了B
的读数,那么每个线程的可能值都是
int a,b;
volatile c;
A0:
a = 10;
b = 5;
c = 20;
A:
int l = c; // c could be either 20 or 0
int j = a; // a could be either 10 or 0
int k = b; // b could be either 5 or 0
c = 50;
B:
if(c == 50){
int l = c; // c can be 20 or 50. c cannot be 0
int j = a; // a could be either 10 or 0
int k = b; // b could be either 5 or 0
}
正如您所看到的,线程A0
的写入与A
或B
的读/写之间没有关系,除非我们有读取前置条件。