Dog*_*Dog 205 java concurrency java-memory-model memory-visibility
今天我实验室的敏感操作完全错了.电子显微镜上的执行器越过它的边界,在一系列事件之后,我损失了1200万美元的设备.我已将故障模块中的40K以上线路缩小到:
import java.util.*;
class A {
static Point currentPos = new Point(1,2);
static class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public static void main(String[] args) {
new Thread() {
void f(Point p) {
synchronized(this) {}
if (p.x+1 != p.y) {
System.out.println(p.x+" "+p.y);
System.exit(1);
}
}
@Override
public void run() {
while (currentPos == null);
while (true)
f(currentPos);
}
}.start();
while (true)
currentPos = new Point(currentPos.x+1, currentPos.y+1);
}
}
Run Code Online (Sandbox Code Playgroud)
我得到的一些输出样本:
$ java A
145281 145282
$ java A
141373 141374
$ java A
49251 49252
$ java A
47007 47008
$ java A
47427 47428
$ java A
154800 154801
$ java A
34822 34823
$ java A
127271 127272
$ java A
63650 63651
Run Code Online (Sandbox Code Playgroud)
由于这里没有任何浮点运算,并且我们都知道有符号整数在Java溢出时表现良好,我认为这段代码没有任何问题.然而,尽管输出指示,该方案没有达到退出条件,它达到了退出条件(这是双方达成并没有达到?).为什么?
我注意到在某些环境中不会发生这种情况.我在64位Linux 上使用OpenJDK 6.
ass*_*ias 140
显然,在读取它之前,对currentPos的写入不会发生,但我不明白这是怎么回事.
currentPos = new Point(currentPos.x+1, currentPos.y+1);
做了一些事情,包括将默认值写入x
和y
(0),然后在构造函数中写入它们的初始值.由于您的对象未安全发布,因此编译器/ JVM可以自由地重新排序这4个写操作.
因此,从阅读线程的角度来看,x
使用其新值进行读取是合法的执行,但y
默认值为0.当您到达println
语句时(顺便说一下,它会同步并因此影响读取操作),变量具有其初始值,程序将打印预期值.
标记currentPos
为volatile
将确保安全发布,因为您的对象实际上是不可变的 - 如果在您的实际用例中,对象在构造后发生变异,则volatile
保证不够,您可能再次看到不一致的对象.
或者,您可以创建Point
不可变的,即使不使用也可以确保安全发布volatile
.要实现不变性,您只需要标记x
和y
最终.
作为附注并且已经提到过,synchronized(this) {}
JVM可以将其视为无操作(我知道你包含它来重现行为).
Ed *_*ese 29
由于currentPos
在线程外部进行了更改,因此应将其标记为volatile
:
static volatile Point currentPos = new Point(1,2);
Run Code Online (Sandbox Code Playgroud)
如果没有volatile,则不保证线程会读取正在主线程中生成的currentPos的更新.因此,继续为currentPos编写新值,但由于性能原因,线程继续使用以前的缓存版本.由于只有一个线程修改了currentPos,因此您可以在没有锁的情况下离开,从而提高性能.
如果您在线程中只读取一次值以用于比较和随后显示它们,则结果会有很大不同.当我做下面x
总是显示为1
和y
之间变化0
和一些大的整数.我认为在没有volatile
关键字的情况下,它的行为在某种程度上是未定义的,并且代码的JIT编译可能有助于它像这样行事.此外,如果我注释掉空synchronized(this) {}
块,那么代码也可以工作,我怀疑这是因为锁定导致了足够的延迟,currentPos
并且其字段被重新读取而不是从缓存中使用.
int x = p.x + 1;
int y = p.y;
if (x != y) {
System.out.println(x+" "+y);
System.exit(1);
}
Run Code Online (Sandbox Code Playgroud)
pau*_*ulj 19
你有普通的内存,'currentpos'引用和Point对象及其后面的字段,在2个线程之间共享,没有同步.因此,在主线程中发生在该存储器中的写入与所创建线程中的读取之间没有定义的顺序(称为T).
主线程正在执行以下写操作(忽略点的初始设置,将导致px和py具有默认值):
因为这些写入在同步/障碍方面没有什么特别之处,所以运行时可以自由地允许T线程看到它们以任何顺序出现(主线程当然总是看到根据程序顺序排序的写入和读取),并且发生在T中读取之间的任何点
所以T正在做:
鉴于main中的写入与T中的读取之间没有排序关系,显然有几种方法可以产生结果,因为在写入currentpos.y或currentpos.x 之前,T可能会看到main对currentpos 的写入:
等等......这里有很多数据竞赛.
我怀疑这里有缺陷的假设是认为这行的结果在执行它的线程的程序顺序中的所有线程中都是可见的:
currentPos = new Point(currentPos.x+1, currentPos.y+1);
Run Code Online (Sandbox Code Playgroud)
Java没有这样的保证(性能很糟糕).如果您的程序需要保证相对于其他线程中的读取的写入顺序,则必须添加更多内容.其他人建议将x,y字段设为最终字段,或者使currentpos具有易变性.
使用final有一个优点,它使字段不可变,从而允许缓存值.使用volatile会导致每次写入和读取currentpos时出现同步,这可能会影响性能.
有关详细信息,请参阅Java语言规范的第17章:http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
(初步回答假设内存模型较弱,因为我不确定JLS保证volatile是否足够.回答编辑以反映来自assylias的评论,指出Java模型更强 - 发生 - 之前是传递 - 并且当前事件的易变性也足够).
归档时间: |
|
查看次数: |
49366 次 |
最近记录: |