今天我实验室的敏感操作完全错了。电子显微镜上的执行器越过它的边界,在一系列事件之后,我损失了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。
答案 0 :(得分:138)
显然,在读取之前写入currentPos并不会发生,但我不知道这是怎么回事。
currentPos = new Point(currentPos.x+1, currentPos.y+1);
做了一些事情,包括将默认值写入x
和y
(0),然后在构造函数中写入它们的初始值。由于您的对象未安全发布,因此编译器/ JVM可以自由地重新排序这4个写操作。
因此,从阅读线程的角度来看,使用其新值读取x
是合法的执行,但y
的默认值为0。当您到达println
语句(顺便说一下它同步并因此影响读取操作)时,变量具有其初始值,程序将打印预期值。
将currentPos
标记为volatile
将确保安全发布,因为您的对象实际上是不可变的 - 如果在您的实际用例中,对象在构造后发生变异,volatile
保证将不够你可以再次看到一个不一致的对象。
或者,即使不使用Point
,您也可以使volatile
不可变,这也将确保安全发布。要实现不变性,您只需标记x
和y
final。
作为旁注,如前所述,JVM可以将synchronized(this) {}
视为无操作(我知道你将其包括在内以重现行为)。
答案 1 :(得分:29)
由于currentPos
在线程之外被更改,因此应将其标记为volatile
:
static volatile Point currentPos = new Point(1,2);
如果没有volatile,则不保证线程会读取正在主线程中生成的currentPos的更新。因此,继续为currentPos编写新值,但由于性能原因,线程继续使用以前的缓存版本。由于只有一个线程修改了currentPos,因此您可以在没有锁定的情况下离开,从而提高性能。
如果您在线程中仅读取一次值以用于比较和随后显示它们,则结果会有很大不同。当我执行以下操作时,x
始终显示为1
,y
在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);
}
答案 2 :(得分:19)
你有普通的内存,'currentpos'引用和Point对象及其后面的字段,在2个线程之间共享,没有同步。因此,在主线程中发生在此内存中的写入与创建的线程中的读取之间没有定义的顺序(称为T)。
主线程正在执行以下写操作(忽略点的初始设置,将导致p.x和p.y具有默认值):
因为这些写入在同步/障碍方面没有什么特别之处,所以运行时可以自由地允许T线程看到它们以任何顺序出现(主线程当然总是看到根据程序顺序排序的写入和读取) ,并在T中的读数之间的任何点发生。
所以T正在做:
鉴于main中的写入与T中的读取之间没有排序关系,显然有几种方法可以产生结果,因为T可能会在写入之前看到main对currentpos 的写入currentpos.y或currentpos.x:
依此类推......这里有很多数据竞赛。
我怀疑这里有缺陷的假设是认为从这一行产生的写操作在执行它的程序的程序顺序中的所有线程中都是可见的:
currentPos = new Point(currentPos.x+1, currentPos.y+1);
Java没有这样的保证(性能很糟糕)。如果您的程序需要保证相对于其他线程中的读取的写入顺序,则必须添加更多内容。其他人建议将x,y字段设为最终字段,或者使currentpos不稳定。
使用final的优点是它使字段不可变,因此允许缓存值。使用volatile会导致每次写入和读取currentpos时出现同步,这可能会影响性能。
有关详细信息,请参阅Java语言规范的第17章:http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
(初步回答假设内存模型较弱,因为我不确定JLS保证volatile是否足够。回答编辑以反映来自assylias的评论,指出Java模型更强 - 发生 - 之前是传递性的 - 并且如此易变currentpos也足够了。)
答案 3 :(得分:-2)
您可以使用对象来同步写入和读取。否则,正如其他人之前所说的那样,对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);
}
}
答案 4 :(得分:-3)
您正在访问currentPos两次,并且不保证在这两次访问之间不会更新它。
例如:
您实际上是在比较两个不同的点。
请注意,即使使currentPos volatile不会保护您,也不会保护您,因为它是工作线程的两次单独读取。
添加
boolean IsValid() { return x+1 == y; }
你的积分课的方法。这将确保在检查x + 1 == y时仅使用currentPos的一个值。