假设有两个没有同步的线程,一个设置n = 1另一个执行method()。
n = 1
method()
在下文中,“读取”始终指字段的读取n。
n
public class MyClass { public int n = 0; public void method() { System.out.println(n); //read 1 System.out.println(n); //read 2 } }
以下输出可能吗?
1 0
答案是肯定的,因为即使读1发生在读2之前,但是由于读2不会改变线程内执行的语义,因此仍可能在读1之前对读2重新排序。
这个推理正确吗?
发生在前并不意味着两个任意操作的顺序。更精确地说,出现这种情况,之前所做的最重要的事情是占用 的写入 和 读取 在 之前发生的一致性 。值得注意的是,它告诉读者可以进行哪些写操作:最后一次写入按发生在先的顺序发生,或者任何其他未按发生在发生在先的顺序进行的写入(种族)。请注意,在不违反该要求的情况下,两次连续读取可能会看到从不同的(读写)写入获得的不同值。
例如JLS 17.4.5说:
应该注意的是,两个动作之间存在先发生关系并不一定意味着在实现中它们必须按照该顺序进行。如果重新排序产生的结果与合法执行相符,则不合法。
这样的数据争夺是令人毛骨悚然的:读写读取会在每次读取时返回令人惊讶的数据,而Java内存模型会捕获这些数据。因此,更精确的答案是产生(1,0)的执行不会违反Java内存模型约束(同步顺序一致性,同步顺序- 程序顺序一致性,先发生的一致性,因果关系要求),因此是允许的。
实施方面:在硬件上,由于它们是独立的,因此无论它们的“程序顺序”如何,这两种负载都可以在不同的时间启动和/或到达内存子系统。在编译器中,指令调度也可以不考虑程序顺序来进行独立读取,从而以“反直观”的顺序将负载暴露给硬件。
如果希望按程序顺序进行 读取 ,则需要更强的属性。JMM将属性赋予同步操作(在您的示例中,创建一个变量volatile将使该操作生效),该属性将这些操作按与程序顺序 一致 的 总 同步顺序进行 绑定 。在这种情况下,将禁止(1,0)。
volatile
一个非常特殊的jcstress测试用例的插图(请参见警告的完整资料):
private final Holder h1 = new Holder(); private final Holder h2 = h1; private static class Holder { int a; int trap; } @Actor public void actor1() { h1.a = 1; } @Actor public void actor2(IntResult2 r) { Holder h1 = this.h1; Holder h2 = this.h2; h1.trap = 0; h2.trap = 0; r.r1 = h1.a; r.r2 = h2.a; }
即使在不重新排序负载的x86上,也会产生(1、0),哎呀:
[OK] o.o.j.t.volatiles.ReadAfterReadTest (fork: #1, iteration #1, JVM args: [-server]) Observed state Occurrences Expectation Interpretation [0, 0] 16,736,450 ACCEPTABLE Doing both reads early. [1, 1] 108,816,262 ACCEPTABLE Doing both reads late. [0, 1] 3,941 ACCEPTABLE Doing first read early, not surprising. [1, 0] 84,477 ACCEPTABLE_INTERESTING First read seen racy value early, and the s...
使为Holder.avolatile将使(1、0)消失。
Holder.a