今天我实验室的一项敏感操作完全出错了。电子显微镜上的一个执行器越过了它的边界,在一系列事件之后,我损失了 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 中溢出时表现良好,我认为这段代码没有任何问题。然而,尽管输出表明程序没有达到退出条件,但它达到了退出条件(既达到 又 没有达到?)。为什么?
我注意到这在某些环境中不会发生。我在 64 位 Linux上使用OpenJDK 6。
显然,在读取它之前不会发生对 currentPos 的写入,但我不明白这可能是什么问题。
currentPos = new Point(currentPos.x+1, currentPos.y+1);做了一些事情,包括将默认值写入x和y(0),然后在构造函数中写入它们的初始值。由于您的对象未安全发布,编译器/JVM 可以自由地重新排序这 4 个写操作。
currentPos = new Point(currentPos.x+1, currentPos.y+1);
x
y
因此,从读取线程的角度来看,x使用其新值但y使用其默认值 0 读取是合法的执行。当您到达println语句时(顺便说一句,它是同步的,因此确实会影响读取操作),变量具有它们的初始值并且程序会打印预期值。
println
标记currentPos为volatile将确保安全发布,因为您的对象实际上是不可变的 - 如果在您的实际用例中,对象在构造后发生了变异,volatile那么保证是不够的,您可能会再次看到不一致的对象。
currentPos
volatile
或者,您可以使Point不可变的,这也将确保安全发布,即使不使用volatile. 要实现不变性,您只需要标记x和y最终。
Point
作为旁注,如前所述,synchronized(this) {}JVM 可以将其视为无操作(我知道您将其包括在内以重现行为)。
synchronized(this) {}