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

Dog*_*Dog 205 java concurrency java-memory-model memory-visibility

今天我实验室的敏感操作完全错了.电子显微镜上的执行器越过它的边界,在一系列事件之后,我损失了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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我得到的一些输出样本:

$ 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
Run Code Online (Sandbox Code Playgroud)

由于这里没有任何浮点运算,并且我们都知道有符号整数在Java溢出时表现良好,我认为这段代码没有任何问题.然而,尽管输出指示,该方案没有达到退出条件,它达到了退出条件(这是双方达成没有达到?).为什么?


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

ass*_*ias 140

显然,在读取它之前,对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可以将其视为无操作(我知道你包含它来重现行为).

  • 我不确定,但不会让x和y最终产生同样的效果,避免内存障碍? (4认同)
  • 更简单的设计是一个不可变的点对象,用于测试构造中的不变量.因此,您永远不会冒险发布危险的配置. (3认同)
  • 不变性本身并不能保证安全发布(如果x是私有的,但只有getter暴露,同样的出版问题仍然存在).final或volatile确保它.我更喜欢最终的挥发性. (2认同)

Ed *_*ese 29

由于currentPos在线程外部进行了更改,因此应将其标记为volatile:

static volatile Point currentPos = new Point(1,2);
Run Code Online (Sandbox Code Playgroud)

如果没有volatile,则不保证线程会读取正在主线程中生成的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);
}
Run Code Online (Sandbox Code Playgroud)

  • 是的,我也可以锁定一切.你想说什么? (2认同)

pau*_*ulj 19

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

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

  • 到px
  • 到py
  • 到currentpos

因为这些写入在同步/障碍方面没有什么特别之处,所以运行时可以自由地允许T线程看到它们以任何顺序出现(主线程当然总是看到根据程序顺序排序的写入和读取),并且发生在T中读取之间的任何点

所以T正在做:

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

鉴于main中的写入与T中的读取之间没有排序关系,显然有几种方法可以产生结果,因为在写入currentpos.y或currentpos.x 之前,T可能会看到main对currentpos 的写入:

  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.写入对T ...等可见
  3. 它首先读取currentpos.y,在y写入发生之前(0),然后在x写入后读取currentpos.x,evals为true.等等

等等......这里有很多数据竞赛.

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

currentPos = new Point(currentPos.x+1, currentPos.y+1);
Run Code Online (Sandbox Code Playgroud)

Java没有这样的保证(性能很糟糕).如果您的程序需要保证相对于其他线程中的读取的写入顺序,则必须添加更多内容.其他人建议将x,y字段设为最终字段,或者使currentpos具有易变性.

  • 如果你使x,y字段成为final,那么Java保证在构造函数返回之前,在所有线程中都会看到它们的值写入.因此,由于对currentpos的赋值在构造函数之后,因此保证T线程以正确的顺序看到写入.
  • 如果你使currentpos易失,那么Java保证这是一个同步点,它将与其他同步点进行总排序.在main中,对x和y的写入必须在写入currentpos之前发生,然后在另一个线程中对currentpos的任何读取也必须看到之前发生的x,y的写入.

使用final有一个优点,它使字段不可变,从而允许缓存值.使用volatile会导致每次写入和读取currentpos时出现同步,这可能会影响性能.

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

(初步回答假设内存模型较弱,因为我不确定JLS保证volatile是否足够.回答编辑以反映来自assylias的评论,指出Java模型更强 - 发生 - 之前是传递 - 并且当前事件的易变性也足够).

  • 在我看来,这是最好的解释.非常感谢! (2认同)
  • 简而言之,在'Point currentPos = new Point(x,y)`中,你有3个写:(w1)`this.x = x`,(w2)`this.y = y`和(w3)`currentPos =新点.程序顺序保证hb(w1,w3)和hb(w2,w3).在程序的后面你读了(r1)`currentPos`.如果`currentPos`不是易失性的,则r1和w1,w2,w3之间没有hb,所以r1可以观察到它们中的任何一个(或没有).使用volatile,你会引入hb(w3,r1).并且hb关系是传递的,所以你也引入了hb(w1,r1)和hb(w2,r1).这在Java Concurrency in Practice(3.5.3.安全发布惯用语)中进行了总结. (2认同)
  • 啊,如果hb以这种方式传递,那么这是一个足够强大的"障碍",是的.我不得不说,确定17.4.5的JLS定义hb具有该属性并不容易.它肯定不在17.4.5开头附近给出的属性列表中.传递闭包仅在一些解释性说明之后进一步提及!无论如何,很高兴知道,谢谢你的答案!:).注意:我会更新我的回答以反映assylias的评论. (2认同)