Java memory visibility documentation说:
在对该相同字段的每次后续读取之前发生对易失性字段的写入。
我很困惑后续在多线程环境中意味着什么。这句话是否意味着所有处理器和内核都有一些全局时钟。那么例如我在某个线程的循环c1中为变量赋值,然后第二个线程能够在后续循环c1 + 1中看到这个值?
答案 0 :(得分:6)
听起来像是在说它在线程之间提供无锁的获取/释放内存排序语义。请参阅Jeff Preshing's article explaining the concept(主要是针对C ++,但文章的主要内容是语言中性和概念。)
实际上,Java std::begin(d) + 20
提供了顺序一致性,而不仅仅是acq / rel。但是,没有实际的锁定。请参阅Jeff Preshing的文章,了解命名与锁定相符的原因。)
如果读者看到你写的值,那么它就知道在写入之前生产者线程中的所有内容都已经发生了。
此排序保证仅适用于在单个帖子中订购的其他保证。
e.g。
volatile
制片:
int data[100];
volatile bool data_ready = false;
消费者:
data[0..99] = stuff;
// release store keeps previous ops above this line
data_ready = true;
如果while(!data_ready){} // spin until we see the write
// acquire-load keeps later ops below this line
int tmp = data[99]; // gets the value from the producer
不是易变的,那么读它就不会在两个线程之间建立一个先发生的关系。
您不必拥有自旋循环,您可以从data_ready
读取序列号或数组索引,然后阅读volatile int
。
我不太了解Java。我认为data[i]
实际上为您提供了顺序一致性,而不仅仅是发布/获取。顺序释放存储不允许在以后加载时重新排序,因此在典型的硬件上,它需要昂贵的内存屏障,以确保在允许任何后续加载执行之前刷新本地核心的存储缓冲区。
Volatile Vs Atomic详细说明了volatile
为您提供的排序。
Java volatile
只是一个排序关键字;它不等同于C11 volatile
或C++11 std::atomic<T>
,它们也为您提供原子RMW操作。在Java中,_Atomic
不是原子增量,它是一个单独的加载和存储,如volatile_var++
。在Java中,您需要像volatile_var = volatile_var + 1
这样的类来获取原子RMW。
请注意,C / C ++ AtomicInteger
并不意味着原子性或排序;它只告诉编译器假设该值可以异步修改。除了最简单的情况之外,这只是你需要为任何事情编写无锁的一小部分。
答案 1 :(得分:3)
我很困惑多线程背景下的后续意义。这句话是否暗示了所有处理器和内核的全局时钟......?
及时之后的后续手段(根据字典)。当然,计算机中的所有CPU都有一个全局时钟(想想X Ghz),文档试图说如果thread-1在时钟标记1处做了某事,那么thread-2会在时钟周期2的另一个CPU上做某事,它& #39; s的行为被认为是后续的。
对易失性字段的写入发生在每次后续读取该字段之前。
可以添加到这句话中以使其更清晰的关键短语是&#34;在另一个帖子中#34;。理解它可能更有意义:
对易失性字段的写入发生在每次后续读取另一个字段中的相同字段之前。
这是说如果读取volatile
字段发生在Thread-2中(及时)在Thread-1中写入,那么Thread-2将得到保证查看更新的值。 documentation you point to中的更多内容是(强调我的)部分:
...只有在写入操作发生在读操作之前,另一个线程才能保证一个线程写入的结果可见。 synchronized和volatile构造以及Thread.start()和Thread.join()方法可以形成先发生关系。特别是。
注意突出显示的短语。只要重新排序不违反语言的定义,Java编译器就可以在任何一个线程的执行中重新排序指令以进行优化 - 这称为执行顺序和与程序顺序截然不同。
让我们看看以下示例,其中变量a
和b
是非易失性整数,初始化为0且没有synchronized
子句。显示的是 program 顺序以及线程遇到代码行的时间。
Time Thread-1 Thread-2
1 a = 1;
2 b = 2;
3 x = a;
4 y = b;
5 c = a + b; z = x + y;
如果Thread-1在时间5添加a + b
,则保证为3
。但是,如果Thread-2在时间5添加x + y
,它可能会得到0,1,2或3取决于竞争条件。为什么?由于效率原因,编译器可能已经重新排序了Thread-1中的指令,以便在a
之后设置b
。此外,Thread-1可能没有正确发布a
和b
的值,因此Thread-2可能会过时。即使Thread-1被上下文切换掉或者跨越写入内存屏障并且发布了a
和b
,Thread-2也需要越过读屏障来更新任何缓存的a
和b
值。
如果a
和b
被标记为volatile
,那么对a
的写入必须发生在之前(就可见性保证而言)后续读取{{1}在第3行,写入a
必须在第4行随后读取b
之前发生。两个线程都会得到3。
我们在java中使用b
和volatile
关键字来确保事先发生的保证。分配synchronized
或退出volatile
块时会超过写入内存屏障,并且在读取synchronized
或输入volatile
块时会超过读屏障。 Java编译器无法通过这些内存屏障重新排序写入指令,因此可确保更新顺序。这些关键字控制指令重新排序并确保正确的内存同步。
注意: synchronized
在单线程应用程序中是不必要的,因为程序顺序可确保读取和写入保持一致。单线程应用程序可能会在第3和第4时看到(非易失性)volatile
和a
的任何值,但由于语言保证,总是在时间5看到3 。因此,尽管使用b
会更改单线程应用程序中的重新排序行为,但只有在线程之间共享数据时才需要它。
答案 2 :(得分:2)
这意味着一旦某个Thread 将写入易失性字段,所有其他线程将观察(在下一次读取时)该写入值;但这并不能保护你免受比赛。
线程有自己的缓存,这些缓存将通过缓存一致性协议无效并使用新写入的值进行更新。
修改强>
后续意味着只要在写入之后发生。由于你不知道发生这种情况的确切周期/时间,你通常会说当其他一些线程观察到写入时,它会观察在写入之前完成的所有操作;因此,一个易失性建立了先发生的保证。
类似于示例:
// Actions done in Thread A
int a = 2;
volatile int b = 3;
// Actions done in Thread B
if(b == 3) { // observer the volatile write
// Thread B is guaranteed to see a = 2 here
}
你也可以循环(旋转等待),直到你看到3为例。
答案 3 :(得分:1)
这更像是不会发生什么的定义,而不是 会发生什么。
基本上它是说一旦<Modules>
<SFModule filename="sfmodule.psm1" varname="SFModule" type="module" sha256="A1B6AE739F733DD8C75AD2778D395953BF2F6EF61B4240B014C446394B87E881" />
<ETSModule filename="etsmodule.psm1" varname="ETSModule" type="module" sha256="46FD0887DDFBDD88FAECD173F41A448BC57E26CEE6FF40C32500E5994284F47B" />
<WPFModule filename="wpfmodule.psm1" varname="WPFModule" type="module" sha256="1BEC1B84778148570774AB4E51126A8FB4F2BA308D5BA391E3198805CC13DB2B" />
<GetInt filename="getint.ps1" varname="getInt" type="script" sha256="FBAF335E80623F26B343939B3D44B9C847388D3ADB351EAF551C8A35D75DF876" />
<GetDom filename="getdom.ps1" varname="getDom" type="script" sha256="70F4DA69E99DA6157D8DFB60368D65B132504114FF6F6FACDE351FF0C8B8F820" />
<CopyResult filename="copy_result.ps1" varname="copyResult" type="script" sha256="DCA12BCF8FAC6F52C6E4721DFA2F77FC78183681F6945CB7FCD2010CA94A86C3" />
</Modules>
变量发生写,就不会有任何其他线程在读取变量时读取过时的值。
考虑以下情况。
主题 A 不断递增atomic
值atomic
。
线程 B 偶尔会读取a
并将该值公开为
非原子 A.a
变量。
主题 C 偶尔会同时读取b
和A.a
。
鉴于B.b
为a
,可以从 C 的角度推断,atomic
偶尔会小于{{1}但是永远不会超过 b
。
如果a
不原子,则无法提供此类保证。在某些缓存情况下, C 很可能会在任何时间看到a
超过a
的进度。
这是一个简单的演示,说明Java内存模型如何允许您推理关于在多线程环境中可以发生什么和不可以发生什么。在现实生活中,读取和写入数据结构之间的潜在竞争条件可能要复杂得多,但推理过程是相同的。
答案 4 :(得分:1)
Peter's answer给出了Java内存模型设计的基本原理 在这个答案中,我试图仅使用JLS中定义的概念进行解释。
在Java中,每个线程都由一组动作组成 其中一些动作有可能被其他线程观察(例如,写一个共享变量),这些 被称为同步动作。
在源代码中写入线程操作的顺序称为程序顺序。
订单定义之前的以及之后的是什么(或更好,之前)。
在一个帖子中,每个动作都有一个发生在之前的关系(用&lt;表示)与 next (按程序顺序)动作。 这种关系很重要,但很难理解,因为它非常基础:它保证如果A&lt; B然后 &#34;效果&#34; A对于B.可见 这确实是我们在编写函数代码时所期望的。
考虑
Thread 1 Thread 2
A0 A'0
A1 A'1
A2 A'2
A3 A'3
然后通过程序顺序我们知道A0&lt; A1&lt; A2&lt; A3和A&#39; 0&lt; A&#39; 1&lt; A&#39; 2&lt;甲&#39; 3
。
我们不知道如何订购所有行动
它可以是A0&lt; A&#39; 0&lt; A&#39; 1&lt; A&#39; 2&lt; A1&lt; A2&lt; A3&lt; A&#39; 3或与素数交换的序列
但是,每个这样的序列必须具有每个线程的单个动作根据线程的程序顺序排序。
两个程序订单不足以订购每个操作,它们是部分订单,与之相反 我们正在寻找总订单。
根据发生的可测量时间(如时钟)将操作连续放置的总顺序称为执行顺序。
这是动作实际发生的顺序(仅请求出现的动作发生在
这个顺序,但这只是一个优化细节)。
到目前为止,动作不是在线程间(在两个不同的线程之间)进行排序的 同步动作就是为了这个目的 每个同步操作至少与另一个同步操作同步(它们通常成对出现,如 写入和读取volatile变量,锁定和互斥锁的解锁。
synchronize-with 关系是线程之间发生的事情(前者暗示后者),它暴露为 一个不同的概念,因为1)它稍微是2)发生 - 之前由硬件自动强制执行同步 可能需要软件干预。
发生之前 - 从程序订单派生,与同步订单同步 - (由&lt;&lt;表示)。
同步顺序是根据两个属性定义的:1)它是一个总顺序2)它与每个线程一致
程序顺序。
让我们为我们的主题添加一些同步动作:
Thread 1 Thread 2
A0 A'0
S1 A'1
A1 S'1
A2 S'2
S2 A'3
程序顺序微不足道 什么是同步顺序?
我们正在寻找1)包括所有S1,S2,S&#39;以及S&#39; 2和2)必须具有S1&lt; S2和S&#39; 1&lt; S&#39; 2。
可能的结果:
S1 < S2 < S'1 < S'2
S1 < S'1 < S'2 < S2
S'1 < S1 < S'2 < S'2
所有都是同步订单,没有一个同步订单但很多,上面的问题是错的,它 应该是&#34; 同步命令是什么?&#34;。
如果S1和S&#1; 1使S1 <&lt;&lt; 1&gt; S&#1; 1比我们将可能的结果限制在S1 <1的情况下。 S&#39; 2所以 结果S&#39; 1&lt; S1&lt; S&#29; 2&lt;现在禁止上述S&#29; 2。
如果S2 <&lt; S&#39;然后唯一可能的结果是S1&lt; S2&lt; S&#39; 1&lt; S&#39; 2,当我相信我们只有一个结果时 顺序一致性(反之亦然)。
注意,如果A&lt;&lt; B这些并不意味着代码中存在强制执行顺序的机制,其中A&lt; B.
同步操作受同步操作影响,它们不会对其进行任何实现
某些同步操作(例如锁定)强制执行特定的执行顺序(从而实现同步顺序),但有些不同意(例如,挥发性的读取/写入)。
它是创建同步顺序的执行顺序,这与synchronize-with关系完全正交。
长话短说,&#34;随后&#34;形容词是指任何有效的同步顺序(根据每个线程 程序顺序)包含所有同步操作的顺序。
然后JLS继续定义何时发生数据竞争(当两个冲突的访问未被发生之前排序时) 在一致之前发生了什么意味着什么 这些都超出了范围。