小编典典

为什么这个 Java 程序会终止,尽管它显然不应该(也不应该)?

all

今天我实验室的一项敏感操作完全出错了。电子显微镜上的一个执行器越过了它的边界,在一系列事件之后,我损失了 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。


阅读 62

收藏
2022-06-27

共1个答案

小编典典

显然,在读取它之前不会发生对 currentPos 的写入,但我不明白这可能是什么问题。

currentPos = new Point(currentPos.x+1, currentPos.y+1);做了一些事情,包括将默认值写入xy(0),然后在构造函数中写入它们的初始值。由于您的对象未安全发布,编译器/JVM
可以自由地重新排序这 4 个写操作。

因此,从读取线程的角度来看,x使用其新值但y使用其默认值 0
读取是合法的执行。当您到达println语句时(顺便说一句,它是同步的,因此确实会影响读取操作),变量具有它们的初始值并且程序会打印预期值。

标记currentPosvolatile将确保安全发布,因为您的对象实际上是不可变的 -
如果在您的实际用例中,对象在构造后发生了变异,volatile那么保证是不够的,您可能会再次看到不一致的对象。

或者,您可以使Point不可变的,这也将确保安全发布,即使不使用volatile. 要实现不变性,您只需要标记xy最终。

作为旁注,如前所述,synchronized(this) {}JVM 可以将其视为无操作(我知道您将其包括在内以重现行为)。

2022-06-27