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

今天我实验室的一个敏感操作完全错了。 电子显微镜上的执行器越过了边界,经过一连串的事件,我损失了1200万美元的设备。 我已经在故障模块中缩小了40多条线路,这是因为:

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 != py) { System.out.println(p.x+" "+py); 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溢出时performance良好,所以我认为这段代码没有问题。 然而,尽pipe产出表明该计划没有达到退出条件,但是达到了退出条件(已经达到未达到?)。 为什么?


我注意到这在某些环境下不会发生。 我在64位Linux上使用OpenJDK 6。

很明显,写到currentPos并没有发生 – 在读它之前,但我不明白这是怎么回事。

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

因此,从阅读线程的angular度来看,使用新值读取x是合法的执行方式,例如, y的默认值为0。 当你到达println语句(顺便说一下,它是同步的,因此确实会影响读操作)时,variables有它们的初始值,程序打印出预期的值。

currentPos标记为volatile将确保安全发布,因为您的对象是有效的不可变的 – 如果在实际使用情况下对象在构build之后发生变化,那么volatile保护将不足以再次看到不一致的对象。

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

作为一个便笺,正如已经提到的, synchronized(this) {}可以被JVM视为一个无操作(我知道你已经包含它来重现行为)。

由于currentPos在线程之外被改变,它应该被标记为volatile

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

如果没有volatile,那么线程就不能保证读入正在主线程中进行的currentPos的更新。 所以新的值继续被写为currentPos,但线程继续使用以前的caching版本的性能的原因。 由于只有一个线程修改了currentPos,所以你可以不用锁就可以提高性能。

如果您只是在线程中只读取一次值,以便在比较和随后的显示中使用,则结果看起来会有很大的不同。 当我做下面的x总是显示为1y0和某个大整数之间变化。 我认为在这一点上它的行为是没有volatile关键字有些未定义的,并且代码的JIT编译可能有助于它的行为。 另外,如果我注释掉空的synchronized(this) {}块,那么代码也可以正常工作,我怀疑这是因为locking会导致足够的延迟,导致currentPos及其字段被重新读取,而不是从caching中使用。

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

你有普通的内存,'currentpos'引用和Point对象及其后面的字段,在2个线程之间共享,没有同步。 因此,在主线程中这个内存发生的写入与创build的线程中的读取(称为T)之间没有定义的顺序。

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

  • 到px
  • py
  • currentpos

由于这些写入在同步/屏障方面没有什么特别之处,所以运行时可以自由地让T线程看到它们以任何顺序发生(当然主线程总是按照程序顺序看写和读),并且发生在T中读取之间的任何点

所以T在做:

  1. 将currentpos读到第
  2. 读取px和py(以任意顺序)
  3. 比较,并采取分支
  4. 读取px和py(任一顺序)并调用System.out.println

由于在main中的写入和在T中的读取之间没有顺序关系,因此可以通过几种方式产生您的结果,因为在写入currentpos.y或currentpos.x 之前 ,T可能会看到main写入currentpos:

  1. 在x写入发生之前,它首先读取currentpos.x – 获得0,然后在发生y写​​入之前读取currentpos.y – 得到0.比较evals为true。 写入变为可见T.调用System.out.println。
  2. 它首先读取currentpos.x,发生x写入后,在发生y写​​入之前读取currentpos.y – 得到0.比较evals为true。 写入对T等可见
  3. 它首先读取currentpos.y,然后在写入之前(0),然后在x写入之后读取currentpos.x,certificate为真。 等等

等等…这里有很多的数据竞赛。

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

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

Java没有这样的保证(这对性能来说是很糟糕的)。 如果你的程序需要保证相对于其他线程的读操作的顺序,那么必须增加更多的东西。 其他人则build议将x,y字段最终确定下来,或者使currentpos变得不稳定。

  • 如果你使x,y字段是最终的,那么Java保证在构造函数返回之前,在所有线程中写入它们的值。 因此,由于对currentpos的赋值在构造函数之后,T线程保证以正确的顺序看到写入。
  • 如果您使currentpos变得不稳定,那么Java保证这是一个同步点,它将与其他同步点进行全序sorting。 就像在main中写入x和y必须发生在写入currentpos之前一样,那么在另一个线程中currentpos的任何读取也必须看到之前发生的x,y的写入。

使用final有一个好处,它使得这些字段是不可变的,从而允许caching这些值。 使用易失性导致在每次写入和读取currentpos时进行同步,这可能会损害性能。

有关详细信息,请参阅Java语言规范的第17章: http : //docs.oracle.com/javase/specs/jls/se7/html/jls-17.html

(初始答案假设为较弱的内存模型,因为我不确定JLS保证的volatile是否足够。回答编辑以反映来自assylias的评论,指出Java模型更强 – 发生之前是过渡性的 – 而且在currentpos上如此易变也足够)。

您可以使用一个对象来同步写入和读取。 否则,正如别人之前所说的,在两次读取p.x + 1和py的中间会写入currentPos

 new Thread() { void f(Point p) { if (p.x+1 != py) { System.out.println(p.x+" "+py); 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); } } 

您正在访问两次currentPos,并且不保证在这两次访问之间没有更新。

例如:

  1. x = 10,y = 11
  2. 工作者线程将px评估为10
  3. 主线程执行更新,现在x = 11和y = 12
  4. 工作者线程评估py为12
  5. 工作线程注意到10 + 1!= 12,所以打印和退出。

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

请注意,即使使currentPos变为volatile也不能保护您,因为这是工作线程的两次单独读取。

添加一个

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

方法到你的点类。 这将确保在检查x + 1 == y时只使用currentPos的一个值。