为什么这个 Java 程序会终止,尽管它显然不应该(也不应该)? [英] Why does this Java program terminate despite that apparently it shouldn't (and didn't)?

查看:17
本文介绍了为什么这个 Java 程序会终止,尽管它显然不应该(也不应该)?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

今天我实验室的一次敏感操作完全出错了.电子显微镜上的一个执行器越过了它的边界,在一连串事件之后,我损失了 1200 万美元的设备.我已经将故障模块中的超过 40K 行缩小到:

import java.util.*;A类{静态点 currentPos = new Point(1,2);静态类点{整数 x;输入 y;点(int x,int y){this.x = x;这.y = y;}}公共静态无效主(字符串 [] args){新线程() {void f(Point p) {同步(这个){}如果 (p.x+1 != p.y) {System.out.println(p.x+" "+p.y);System.exit(1);}}@覆盖公共无效运行(){while (currentPos == null);而(真)f(currentPos);}}.开始();而(真)currentPos = new Point(currentPos.x+1, currentPos.y+1);}}

我得到的一些输出示例:

$ java A145281 145282$ java A141373 141374$ java A49251 49252$ java A47007 47008$ java A47427 47428$ java A154800 154801$ java A34822 34823$ java A127271 127272$ java A63650 63651

由于这里没有任何浮点运算,而且我们都知道有符号整数在 Java 中溢出时表现良好,我认为这段代码没有任何问题.然而,尽管输出表明程序没有达到退出条件,但它达到了退出条件(既达到未达到?).为什么?

<小时>

我注意到在某些环境中不会发生这种情况.我在 64 位 Linux 上使用 OpenJDK 6.

解决方案

显然,写入 currentPos 不会发生在读取它之前,但我不明白这是怎么回事.

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

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

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

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

作为旁注,正如已经提到的,synchronized(this) {} 可以被 JVM 视为无操作(我知道您包含它是为了重现行为).

A sensitive operation in my lab today went completely wrong. An actuator on an electron microscope went over its boundary, and after a chain of events I lost $12 million of equipment. I've narrowed down over 40K lines in the faulty module to this:

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);
    }
}

Some samples of the output I'm getting:

$ 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

Since there isn't any floating point arithmetic here, and we all know signed integers behave well on overflow in Java, I'd think there's nothing wrong with this code. However, despite the output indicating that the program didn't reach the exit condition, it reached the exit condition (it was both reached and not reached?). Why?


I've noticed this doesn't happen in some environments. I'm on OpenJDK 6 on 64-bit Linux.

解决方案

Obviously the write to currentPos doesn't happen-before the read of it, but I don't see how that can be the issue.

currentPos = new Point(currentPos.x+1, currentPos.y+1); does a few things, including writing default values to x and y (0) and then writing their initial values in the constructor. Since your object is not safely published those 4 write operations can be freely reordered by the compiler / JVM.

So from the perspective of the reading thread, it is a legal execution to read x with its new value but y with its default value of 0 for example. By the time you reach the println statement (which by the way is synchronized and therefore does influence the read operations), the variables have their initial values and the program prints the expected values.

Marking currentPos as volatile will ensure safe publication since your object is effectively immutable - if in your real use case the object is mutated after construction, volatile guarantees won't be enough and you could see an inconsistent object again.

Alternatively, you can make the Point immutable which will also ensure safe publication, even without using volatile. To achieve immutability, you simply need to mark x and y final.

As a side note and as already mentioned, synchronized(this) {} can be treated as a no-op by the JVM (I understand you included it to reproduce the behaviour).

这篇关于为什么这个 Java 程序会终止,尽管它显然不应该(也不应该)?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆