与 Java 中的 volatile 字段和同步块的关系发生在之前 - 以及它们对非易失性变量的影响?
Posted
技术标签:
【中文标题】与 Java 中的 volatile 字段和同步块的关系发生在之前 - 以及它们对非易失性变量的影响?【英文标题】:Happens-before relationships with volatile fields and synchronized blocks in Java - and their impact on non-volatile variables? 【发布时间】:2013-06-11 02:32:47 【问题描述】:我对线程的概念仍然很陌生,并尝试更多地了解它。最近,我在 What Volatile Means in Java 上看到了 Jeremy Manson 的一篇博文,他写道:
当一个线程写入一个 volatile 变量,而另一个线程看到 那篇文章,第一个线程告诉第二个线程关于 all 内存的内容,直到它执行对该易失性的写入 多变的。 [...] 所有线程 1 看到的内存内容,之前 它写给
[volatile] ready
,在它之后必须对线程2可见 为ready
读取值true
。 [重点由我自己补充]
现在,这是否意味着在写入 volatile 变量时线程 1 的内存中保存的所有变量(无论是否为 volatile)都将在线程 2 读取该 volatile 变量后变为可见?如果是这样,是否有可能从官方 Java 文档/Oracle 源中将该语句拼凑起来?从哪个版本的 Java 开始可以使用?
特别是,如果所有线程共享以下类变量:
private String s = "running";
private volatile boolean b = false;
并且线程1首先执行以下操作:
s = "done";
b = true;
然后线程 2 之后执行(在线程 1 写入 volatile 字段之后):
boolean flag = b; //read from volatile
System.out.println(s);
这能保证打印“完成”吗?
如果我不将b
声明为volatile
,而是将写入和读取放入synchronized
块中,会发生什么情况?
另外,在题为“Are static variables shared between threads?”的讨论中,@TREE writes:
不要使用 volatile 来保护多个共享状态。
为什么?(抱歉;我还不能对其他问题发表评论,否则我会在那里问...)
【问题讨论】:
可能的欺骗:***.com/questions/12438464/… 【参考方案1】:是的,线程 2 保证会打印 "done" 。当然,如果线程 1 中对 b
的写入实际上发生在线程 2 中对 b
的读取之前,而不是同时发生,或者更早发生!
这里推理的核心是happens-before relationship。多线程程序执行被视为由事件组成。事件可以通过happens-before关系关联起来,即一个事件发生在另一个事件之前。即使两个事件不直接相关,如果您可以跟踪从一个事件到另一个事件的一系列发生之前的关系,那么您可以说一个发生在另一个之前。
在您的情况下,您有以下事件:
线程 1 写入s
线程 1 写入 b
线程 2 从 b
读取
线程 2 从 s
读取
以下规则开始发挥作用:
“如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)。” (节目顺序规则) “对易失性字段 (§8.3.1.4) 的写入发生在对该字段的每次后续读取之前。” (volatile 规则)因此存在以下happens-before关系:
线程 1 写入s
发生在 线程 1 写入 b
之前(程序顺序规则)
线程 1 写入 b
发生在 线程 2 读取 b
之前(易失规则)
线程 2 从 b
读取发生在 线程 2 从 s
读取(程序顺序规则)
如果您遵循该链,您可以看到结果:
线程 1 写入s
发生在 线程 2 从 s
读取之前
【讨论】:
啊。好的。因此happens before
规则适用于线程之间的所有变量(无论是否易失),一个变量是易失的。谢谢。每天学习新东西。
关于引用的博客声明,答案是正确的。该语句正确地表明读取器线程必须已从布尔变量中读取值true
。但是问题的代码示例并没有检查这一点。所以对于问题的代码示例,答案必须是“不,不能保证打印"done"
”。
实际上,我不会如此明确地为这个例子声明任何保证。仅当线程 1 和 2 是唯一涉及的,并且仅当显示的操作是这些线程完成的唯一操作时,它才有效。对于一个不同的例子,我们有class MyBean final String msg; final boolean status;
和一些volatile MyBean b;
,然后在线程1b = new MyBean("done", true);
和线程2MyBean b1 = b;
中,一个更强有力的保证将成立。这就是在实践中正确实施通过volatile
安全发布的方式。
@MarkoTopolnik 你说得对,这只适用于这些情况。它也只有在 JVM 被正确实现并且没有人在中途关闭计算机的情况下才有效。在这样的假设性问题中,所有这些事情通常都被认为是给定的。
我认为它是缓存更新...每次线程写入 volatile 时,它都会将缓存刷新到主内存,如果在 thread1 之后对该 volatile 执行读取操作写入,它会更新它的缓存(线程 2),因此它会看到更新的变量,这些变量是在 volatile 变量之前写入的。 (不是在 volatile 写入之后编写的。)这就是在涉及 volatile 时不允许重新排序的原因。如果两者同时执行(读取和写入),它将面临一些与并发相关的问题。如果这有帮助,请点赞。【参考方案2】:
如果我没有将 b 声明为 volatile,而是将写入和读取放入同步块中,会发生什么情况?
当且仅当您使用相同的锁保护所有此类同步块,您将获得与 volatile
示例相同的可见性保证。此外,您还可以互斥执行此类同步块。
不要使用 volatile 来保护多个共享状态。
为什么?
volatile
不保证原子性:在您的示例中,s
变量也可能在您显示的写入后被其他线程改变;阅读线程不能保证它看到的值。在您读取 volatile
之后但在读取 s
之前写入 s
也是如此。
什么是安全的并且在实践中完成的是共享不可变状态可从写入volatile
变量的引用传递访问。所以也许这就是“一个共享状态”所要表达的意思。
是否有可能从官方 Java 文档/Oracle 源中将该语句拼凑在一起?
引自规范:
17.4.4。同步顺序
对 volatile 变量 v(第 8.3.1.4 节)的写入与任何线程对 v 的所有后续读取同步(其中“后续”根据同步顺序定义)。
17.4.5。下单前发生
如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)。
如果动作 x 与后续动作 y 同步,那么我们也有 hb(x, y)。
这应该足够了。
从哪个版本的 Java 开始可以使用?
Java 语言规范,第 3 版介绍了内存模型规范的重写,这是上述保证的关键。请注意,大多数以前的版本都表现得好像有保证,并且许多代码行实际上都依赖于它。当人们发现这些保证实际上并不存在时,他们感到很惊讶。
【讨论】:
请用简单的术语解释一下。“什么是安全的,并且在实践中完成的是共享从写入到 volatile 变量的引用中可传递访问的不可变状态。所以也许这就是“一个共享状态”。我不明白...什么是传递的不可变状态。 它可以从 volatile 变量中“传递访问”。var.x.y
-- 这里的y
可以从var
传递访问。【参考方案3】:
这能保证打印“完成”吗?
正如Java Concurrency in Practice中所说:
当线程
A
写入volatile
变量并随后 线程B
读取同一个变量,所有变量的值 在写入volatile
变量之前对 A 可见 读取volatile
变量后对 B 可见。
所以YES,这保证打印“完成”。
如果我没有将 b 声明为 volatile,而是将 写入和读取同步块?
这也将保证相同。
不要使用 volatile 来保护多个共享状态。
为什么?
因为,volatile 只保证可见性。它不保证原子性。如果我们在一个方法中有两个 volatile 写入,而线程 A
正在访问另一个线程 B
正在访问这些 volatile 变量,那么当线程 A
正在执行该方法时,线程 A
可能将在操作中间被线程B
抢占(例如,在第一次易失性写入之后但在线程A
进行第二次易失性写入之前)。所以保证synchronization
操作的原子性是最可行的出路。
【讨论】:
"..在写入 volatile 变量之前对 A 可见的所有变量的值在读取 volatile 变量后对 B 可见"。是的,有时在同步上被称为“pyggy-backing”,经常用于并发优化。所以是的,这保证打印“完成”。 @IvanVoroshilin 感谢您对“pyggy-backing”的宝贵见解。您能否提供此事实的链接以获取更多详细信息?以上是关于与 Java 中的 volatile 字段和同步块的关系发生在之前 - 以及它们对非易失性变量的影响?的主要内容,如果未能解决你的问题,请参考以下文章