重要编辑我知道发生两项任务的线程中的“发生之前” 我的问题是是否可能 em>线程读取“b”非空,而“a”仍为空。所以我知道如果你从与之前调用 setBothNonNull(...)的线程相同的线程中调用 doIt(),那么它就不能抛出NullPointerException。但是如果一个人从另一个线程调用 doIt() 而不是调用 setBothNonNull(...)的那个呢?
请注意,此问题仅涉及volatile
关键字和volatile
保证:关于synchronized
关键字的不(所以请不要回答“你必须使用同步”,因为我没有任何问题需要解决:我只是想了解关于无序执行的volatile
保证(或缺乏保证)。
假设我们有一个包含两个volatile
String引用的对象,它们被构造函数初始化为null,并且我们只有一种方法可以修改两个String:通过调用 setBoth(...)< / em>并且我们之后只能将它们的引用设置为非null引用(只允许构造函数将它们设置为null)。
例如(这只是一个例子,毫无疑问):
public class SO {
private volatile String a;
private volatile String b;
public SO() {
a = null;
b = null;
}
public void setBothNonNull( @NotNull final String one, @NotNull final String two ) {
a = one;
b = two;
}
public String getA() {
return a;
}
public String getB() {
return b;
}
}
在 setBothNoNull(...)中,指定非空参数“a”的行出现在指定非空参数“b”的行之前。
然后,如果我这样做(再一次,毫无疑问,接下来会出现问题):
doIt() {
if ( so.getB() != null ) {
System.out.println( so.getA().length );
}
}
我的理解是正确的,由于无序执行,我可以得到 NullPointerException ?
换句话说:不能保证因为我读了一个非空的“b”我会读一个非空的“a”吗?
因为无序(多)处理器和volatile
工作方式“b”可以在“a”之前分配?
volatile
保证写入后的读取总是会看到最后写入的值,但是这里有一个无序的“问题”对吗? (再一次,“问题”是为了试图理解volatile
关键字和Java内存模型的语义,而不是解决问题)。
答案 0 :(得分:25)
不,你永远不会得到NPE。这是因为volatile
也具有引入先发生关系的记忆效应。换句话说,它将阻止
a = one;
b = two;
如果one
已经具有值a
,则上述语句将不会重新排序,并且b
的所有主题都会观察到值two
。
以下是David Holmes解释的一个主题:
http://markmail.org/message/j7omtqqh6ypwshfv#query:+page:1+mid:34dnnukruu23ywzy+state:results
EDIT(对后续行动的回应): Holmes所说的是,如果只有线程A,编译器可以在理论上进行重新排序。但是,还有其他线程,并且它们可以检测到重新排序。这就是不允许编译器进行重新排序的原因。 java内存模型要求编译器专门确保没有线程会检测到这种重新排序。
但是如果有人从中调用doIt()会怎么样呢 另一个线程比一个调用 setBothNonNull(...)?
不,你仍然永远不会有NPE。 volatile
语义确实强加了线程间排序。这意味着,对于所有现有线程,one
的分配在two
的分配之前发生。
答案 1 :(得分:8)
我的理解是正确的,由于乱序执行,我可以获得NullPointerException吗?换句话说:不能保证因为我读了一个非空的“b”我会读一个非空的“a”吗?
假设分配给a
和b
或非空的值,我认为您的理解不正确。 JLS说:
( 1 )如果x和y是同一个线程的动作,而x在程序顺序中位于y之前,那么hb(x,y)。
( 2 )如果某个动作x与后续动作y同步,那么我们也有hb(x,y)。
( 3 )如果是hb(x,y)和hb(y,z),那么hb(x,z)。
和
( 4 )对volatile变量(第8.3.1.4节)
v
的写入与任何线程的v
的所有后续读取同步(其中后续根据同步顺序定义)。
<强>定理强>
鉴于线程#1已调用
setBoth(...);
一次,并且参数为非null,并且线程#2已观察到b
为非null,则线程#2不能将a
视为空。
非正式证明
换句话说,将a
的非空值写入“在读取a
的值(XXX)之前”。 XXX可以为null的唯一方法是,如果有其他操作将null写入a
,使得hb(写入(a,非空),写入(a,XXX))和hb(写入(a) ,XXX),阅读(a,XXX))。根据问题定义,这是不可能的,因此XXX不能为空。 QED。
解释 - JLS声明hb(...)(“之前发生”)关系并不完全禁止重新排序。但是,如果hb(xx,yy),则如果结果代码具有与原始序列相同的可观察效果,则允许重新排序动作xx和yy仅 。
答案 2 :(得分:2)
我发现以下帖子解释了volatile在这种情况下具有与synchronized相同的排序语义。 Java Volatile is Powerful
答案 3 :(得分:2)
虽然斯蒂芬·C和公认的答案很好并且几乎涵盖了它,但值得注意的是,变量 a 并不是必须的。不稳定 - 你仍然没有获得NPE。
这是因为a = one
和b = two
之间会出现先前发生过的关系,无论a
是volatile
。所以Stephen C的正式证据仍然适用,不需要a
易变。
答案 4 :(得分:0)
我读了这个page,发现了一个非挥发性的&amp;您的问题的非同步版本:
class Simple {
int a = 1, b = 2;
void to() {
a = 3;
b = 4;
}
void fro() {
System.out.println("a= " + a + ", b=" + b);
}
}
fro
可以为a
的值获得1或3,并且b
的值可以单独获得2或4。
(我意识到这并没有回答你的问题,但它补充了它。)