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


因为currentPos是在线程外部被修改的,所以它应该被标记为volatile:

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

没有volatile,线程不能保证读入主线程中对currentPos的更新。因此,将继续为currentPos写入新的值,但出于性能原因,线程将继续使用以前缓存的版本。因为只有一个线程修改currentPos,所以你可以不使用锁,这将提高性能。

The results look much different if you read the values only a single time within the thread for use in the comparison and subsequent displaying of them. When I do the following x always displays as 1 and y varies between 0 and some large integer. I think the behavior of it at this point is somewhat undefined without the volatile keyword and it's possible that the JIT compilation of the code is contributing to it acting like this. Also if I comment out the empty synchronized(this) {} block then the code works as well and I suspect it is because the locking causes sufficient delay that currentPos and its fields are reread rather than used from the cache.

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

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

显然,在读取currentPos之前,不会发生对currentPos的写入,但我不认为这可能是问题所在。

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

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

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

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

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


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

主线程正在执行以下写入操作(忽略point的初始设置,将导致p.x和p.y具有默认值):

对p.x 对p.y 对currentpos

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

T是这样的:

读取currentpos到p 读p.x和p.y(任意顺序) 比较一下,取树枝 读取p.x和p.y(任意顺序)并调用System.out.println

假设main中的写操作和T中的读操作之间没有顺序关系,显然有几种方式可以产生结果,因为T可能会看到main在写入currentpos之前写入currentpos。Y或currentpos.x:

It reads currentpos.x first, before the x write has occurred - gets 0, then reads currentpos.y before the y write has occurred - gets 0. Compare evals to true. The writes become visible to T. System.out.println is called. It reads currentpos.x first, after the x write has occurred, then reads currentpos.y before the y write has occurred - gets 0. Compare evals to true. Writes become visible to T... etc. It reads currentpos.y first, before the y write has occurred (0), then reads currentpos.x after the x write, evals to true. etc.

等等……这里有很多数据竞争。

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

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

Java没有这样的保证(这对性能来说很糟糕)。如果您的程序需要保证相对于其他线程的读操作的写操作顺序,则必须添加更多的东西。其他人建议将x、y字段设为final,或者将currentpos设为volatile。

If you make the x,y fields final, then Java guarantees that the writes of their values will be seen to occur before the constructor returns, in all threads. Thus, as the assignment to currentpos is after the constructor, the T thread is guaranteed to see the writes in the correct order. If you make currentpos volatile, then Java guarantees that that this is a synchronisation point which will be total-ordered wrt other synchronisation points. As in main the writes to x and y must happen before the write to currentpos, then any read of currentpos in another thread must see also the writes of x, y that happened before.

使用final的优点是它使字段不可变,从而允许缓存值。使用volatile会导致每次写入和读取currentpos时都要同步,这可能会影响性能。

详见Java语言规范的第17章:http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html

(最初的答案假设一个较弱的内存模型,因为我不确定JLS保证的volatile是否足够。回答被编辑以反映来自assylias的评论,指出Java模型更强——happens-before是可传递的——所以在currentpos上不稳定也足够了)。


您访问了currentPos两次,并且不能保证在这两次访问之间不会更新它。

例如:

X = 10, y = 11 工作线程计算p.x为10 主线程执行更新,现在x = 11和y = 12 工作线程计算p.y为12 工作线程注意到10+1 != 12,因此打印并退出。

你实际上是在比较两个不同的点。

请注意,即使将currentPos设置为volatile也不能保护您不受此影响,因为它是工作线程的两次单独读取。

添加一个

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

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


您可以使用对象来同步写入和读取。否则,正如其他人前面所说,对currentPos的写入将发生在两次读取p.x+1和p.y的中间。

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