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

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


答案 1

显然,在读取它之前,写入currentPos不会发生,但我不明白这怎么会是问题所在。

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

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

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

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

作为旁注和已经提到的,JVM可以将其视为no-op(我知道您包含它以重现行为)。synchronized(this) {}


答案 2

由于 正在线程外部更改,因此应将其标记为:currentPosvolatile

static volatile Point currentPos = new Point(1,2);

如果不易失性,则不保证线程能够读取在主线程中对 currentPos 进行的更新。因此,由于性能原因,将继续为 currentPos 写入新值,但线程继续使用以前的缓存版本。由于只有一个线程修改了currentPos,因此您可以在没有锁定的情况下离开,这将提高性能。

如果在线程中只读取一次值,以便在比较和随后显示值时使用,则结果看起来会大不相同。当我执行以下操作时,始终显示为和在一些大整数之间变化。我认为在这一点上,如果没有关键字,它的行为有些未定义,并且代码的JIT编译可能有助于它像这样运行。另外,如果我注释掉了空块,那么代码也可以工作,我怀疑这是因为锁定导致足够的延迟,并且它的字段被重新读取而不是从缓存中使用。x1y0volatilesynchronized(this) {}currentPos

int x = p.x + 1;
int y = p.y;

if (x != y) {
    System.out.println(x+" "+y);
    System.exit(1);
}

推荐