与 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 字段和同步块的关系发生在之前 - 以及它们对非易失性变量的影响?的主要内容,如果未能解决你的问题,请参考以下文章

synchronized同步块和volatile同步变量

java volatile总结

为啥将 volatile 与同步块一起使用?

正确使用 Volatile 变量

Java 理论与实践: 正确使用 Volatile 变量

2.3.7synchronized代码块有volatile同步的功能