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

2019-05-12 11:40发布

在我的实验室的操作灵敏走到今天完全错误的。 用电子显微镜的促动器走了过去其边界,以及一连串的事件后,我失去了$ 12百万的设备。 我已经缩小了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中,我会觉得有什么错码。 然而,尽管输出指示,该方案没有达到退出条件,它达到了退出条件(这是双方达成没有达到?)。 为什么?


我发现这并不在某些环境中发生。 我在OpenJDK的 64位Linux 6。

Answer 1:

显然,写入currentPos不会发生,之前它的读,但我看不出如何能成为问题。

currentPos = new Point(currentPos.x+1, currentPos.y+1); 做了一些事情,包括写默认值xy (0),然后在构造函数编写它们的初始值。 因为你的对象没有被安全地公布那些4个的写操作可以通过编译器/ JVM可以自由地重新排序。

因此,从读线程的角度来看,这是一个法律执行读取x与新的价值,但y以0例如它的默认值。 到时候你到达println语句(其中的方式是同步的,因此不会影响读取操作),这些变量的初始值和程序输出的预期值。

标记currentPosvolatile将确保安全的出版物,因为你的目标是有效地不变-如果你的真实使用情况下的对象施工后发生突变, volatile的保证是不够的,你可以再次看到不一致的对象。

另外,您也可以使Point不变而这也将确保安全的出版物,即使不使用volatile 。 为了实现不变性,你只需要标记xy决赛。

作为一个侧面说明,并为已经提到的, synchronized(this) {}可以被视为由JVM(我明白你包括它重现行为)无操作。



Answer 2:

由于currentPos正在改变它的线程应该注明外volatile

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

如果没有挥发性线程不能保证读取更新到在主线程正在取得currentPos。 因此,新的价值观继续currentPos写入,但线程继续使用以前的缓存版本的性能的原因。 由于只有一个线程修改currentPos你可以逃脱不锁,这将提高性能。

如果您在线程中读取的值只有一个时间,在比较使用和随后显示它们的结果看起来非常不同。 当我做下列x总是显示为1y之间变化0和一些大的整数。 我想起来了,在这一点上的行为是没有多少有些不确定的volatile的关键字,它有可能是代码的JIT编译的促进作用是这样的。 另外,如果我注释掉空synchronized(this) {}块,然后代码工作,以及和我怀疑这是因为锁定引起足够的延迟是currentPos及其字段重新读取,而不是从缓存中使用。

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

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


Answer 3:

你有普通存储器中,“currentpos”参考和点对象和后面其字段,2个线程之间共享,而不同步。 因此,有可能发生这种记忆在主线程和所创建的线程的读取写入之间没有定义排序(称之为T)。

主线程执行以下操作,写操作(忽略点的初始设置,将导致像素和PY具有默认值):

  • 以PX
  • 到PY
  • 到currentpos

因为没有什么特别之处的同步/障碍方面这些写,运行时是免费的,允许的T线看到他们出现在任何顺序(当然主线始终看到写入,并根据程序顺序读取命令),并发生在任何点之间的在T.读取

因此T是这样做的:

  1. 读currentpos为P
  2. 读像素和PY(以任一次序)
  3. 比较,并采取分支
  4. 阅读PX和PY(或排序),并调用的System.out.println

由于有在主写操作之间没有排序关系,以及读取T,明显有几种方式这可能会产生的结果,为T可以看到主要的写入currentpos写操作之前 currentpos.y或currentpos.x:

  1. 它读取currentpos.x第一,已发生X写之前 - 得到0,则发生在y写入之前读取currentpos.y - 得到0 evals比较为true。 成为可见的T.的System.out.println写操作时被调用。
  2. 它读取currentpos.x第一,发生在x写操作后,那么发生在y写入之前读取currentpos.y - 得到0 evals比较为true。 写成为可见的...等等。
  3. 它首先读取currentpos.y,发生在Y写入前(0),那么x写后读currentpos.x,evals为true。 等等

等等......还有一些数据在这里比赛。

我怀疑这里有缺陷的假设是认为,从这个线产生的写入是由横跨在线程中执行它的程序顺序的所有线程可见:

currentPos = new Point(currentPos.x+1, currentPos.y+1);

Java使得没有这样的保证(这将会是可怕的性能)。 如果你的程序需要写入其他线程相对于读取的保证排序更多的东西必须加入。 也有人建议使X,Y字段最后,或者可替换地使currentpos挥发性。

  • 如果您在X,Y字段决赛,那么Java保证了它们的值的写入将被视为构造函数返回之前发生的,在所有的线程。 因此,分配给currentpos是构造后时,T线是保证看到正确的顺序写入。
  • 如果您currentpos挥发,那么Java保证,这是一个同步点,这将是总排序的WRT等同步点。 作为主要的写入x和y必须在写之前currentpos发生,然后currentpos在另一个线程中读取也必须看到以前发生x的写,Y。

使用最后的优点是,它使场不可变的,因此允许被缓存的值。 使用挥发性导致在每次写入同步和currentpos,这可能会影响性能的阅读。

请参阅Java语言规范的第17章的血淋淋的细节: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html

(最初的回答假设一个较弱的存储模型,因为我不知道在JLS保证挥发性足以已答复编辑,以反映来自assylias评论,指出了Java模型更强 - 之前发生的传递 - 等挥发性物质在currentpos也足够)。



Answer 4:

你可以使用一个对象,并读取写入同步。 否则,在别人面前说,将发生在两人的中间currentPos写入读取p.x + 1个PY

new Thread() {
    void f(Point p) {
        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();
Object sem = new Object();
while (true) {
    synchronized(sem) {
        currentPos = new Point(currentPos.x+1, currentPos.y+1);
    }
}


Answer 5:

您正在访问currentPos两次,并提供不能保证它没有被更新在这两个访问之间。

例如:

  1. X = 10,Y = 11
  2. 工作者线程评估PX 10
  3. 主线程执行更新,现在X = 11和y = 12
  4. 工作者线程评估PY 12
  5. 工作线程注意到10 + 1!= 12,所以打印后退出。

你基本上是比较两个不同点。

请注意,即使使currentPos挥发性不会保护你从这个,因为它是两个单独的读取由工作线程。

添加

boolean IsValid() { return x+1 == y; }

方法你点类。 这将确保检查X + 1 ==ý当仅一个currentPos的值被使用。



文章来源: Why does this Java program terminate despite that apparently it shouldn't (and didn't)?