今天我实验室的一项敏感操作完全出错了。电子显微镜上的执行器超出了其边界,在发生一系列事件后,我损失了 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);
}
}
我得到的一些输出示例:
$ 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
由于这里没有任何浮点运算,而且我们都知道有符号整数在 Java 中溢出时表现良好,所以我认为这段代码没有任何问题。然而,尽管输出表明程序没有达到退出条件,但它达到了退出条件(它都达到了and还没到?)。为什么?
我注意到这在某些环境中不会发生。我上线了OpenJDK http://en.wikipedia.org/wiki/OpenJDK6 在 64 位 Linux 上。
显然,对 currentPos 的写入不会发生在读取之前,但我不明白这怎么会成为问题。
currentPos = new Point(currentPos.x+1, currentPos.y+1);
做一些事情,包括将默认值写入x
and y
(0) 然后将它们的初始值写入构造函数中。由于您的对象未安全发布,因此编译器/JVM 可以自由地重新排序这 4 个写入操作。
所以从读取线程的角度来看,读取是合法执行的x
具有新的价值,但是y
例如,其默认值为 0。当您到达println
语句(顺便说一下,它是同步的,因此会影响读取操作),变量有其初始值,程序会打印预期值。
Marking currentPos
as volatile
将确保安全发布,因为您的对象实际上是不可变的 - 如果在您的实际用例中该对象在构造后发生了变化,volatile
保证还不够,您可能会再次看到不一致的对象。
或者,您可以使Point
不可变,即使不使用,也将确保安全发布volatile
。要实现不变性,您只需标记x
and y
final.
作为旁注,正如已经提到的,synchronized(this) {}
可以被 JVM 视为无操作(我知道您将其包含在内是为了重现该行为)。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)