在 Java 中与易失性字段和同步块的关系之前发生 - 以及它们对非易失性变量的影响?

我对线程的概念还很陌生,并试图更多地了解它。最近,我偶然发现了Jeremy Manson的一篇关于Java中Volvel means的博客文章,他写道:

当一个线程写入易失性变量,而另一个线程看到该写入时,第一个线程会告诉第二个线程内存的所有内容,直到它对该易失性变量执行写入。[...]线程 1 看到的所有内存内容,在写入 之前,在读取 的值后,必须对线程 2 可见。[着重号由我自己添加][volatile] readytrueready

现在,这是否意味着在写入易失性变量时,线程 1 内存中保存的所有变量(无论是否易失性)在读取该易失性变量后对线程 2 可见?如果是这样,是否有可能从官方Java文档/ Oracle来源中将该声明混淆在一起?从哪个版本的Java开始,这将起作用?

特别是,如果所有线程共享以下类变量:

private String s = "running";
private volatile boolean b = false;

线程 1 首先执行以下命令:

s = "done";
b = true;

然后线程 2 在之后执行(在线程 1 写入易失性字段之后):

boolean flag = b; //read from volatile
System.out.println(s);

这能保证打印“完成”吗?

如果我没有将b声明为易失性,而是将写入和读取放入同步块中,会发生什么情况?

此外,在标题为“静态变量是否在线程之间共享?”的讨论中,@TREE写道

不要使用易失性来保护多个共享状态。

为什么?(对不起;我还不能评论其他问题,否则我会在那里问...)


答案 1

是的,可以保证线程2将打印“完成”。当然,如果线程 1 中的写入实际上发生在线程 2 中的读取之前,而不是同时发生或更早发生!bb

这里推理的核心是事前发生的关系。多线程程序执行被视为由事件组成。事件可以通过发生在之前的关系来关联,这种关系表示一个事件发生在另一个事件之前。即使两个事件没有直接关系,如果你能追踪从一个事件到另一个事件的发生之前关系链,那么你可以说一个发生在另一个事件之前。

在您的情况下,您有以下事件:

  • 线程 1 写入s
  • 线程 1 写入b
  • 线程 2 读取自b
  • 线程 2 读取自s

以下规则开始发挥作用:

  • “如果 x 和 y 是同一线程的操作,并且 x 在程序顺序上位于 y 之前,则 hb(x, y)。(程序顺序规则)
  • “写入易失性字段 (§8.3.1.4) 时,每次读取该字段之前都会发生。(易失性规则)

因此,存在以下先发生关系:

  • 线程 1 写入 s 发生在线程 1 写入 b 之前(程序顺序规则)
  • 线程 1 写入 b 发生在线程 2 从 b 读取之前(易失性规则)
  • 线程 2 从 b 读取发生在线程 2 从 s 读取之前(程序顺序规则)

如果您遵循该链,则结果如下:

  • 线程 1 写入 s 发生在线程 2 从 s 读取之前

答案 2

如果我没有将b声明为易失性,而是将写入和读取放入同步块中,会发生什么情况?

当且仅当您使用相同的锁保护所有此类同步块时,您才能像您的示例一样保证可见性。此外,您还可以相互排除此类同步块的执行。volatile

不要使用易失性来保护多个共享状态。

为什么?

volatile不保证原子性:在您的示例中,变量也可能在您显示的写入后被其他线程突变;读取线程将不能保证它看到哪个值。同样的事情也适用于在您读取 之后发生的写入,但在读取 之前。ssvolatiles

在实践中,可以安全地执行的是共享不可变状态,该状态可从写入变量的引用传递访问。因此,也许这就是“一块共享状态”的含义。volatile

是否有可能从官方Java文档/ Oracle来源中将该声明一起混淆?

引用来自规范:

17.4.4. 同步顺序

对易失性变量 v (§8.3.1.4) 的写入与任何线程对 v 的所有后续读取同步(其中“后续”是根据同步顺序定义的)。

17.4.5. 订单前发生

如果 x 和 y 是同一线程的操作,并且 x 在程序顺序上位于 y 之前,则 hb(x, y)。

如果一个动作 x 与一个后续的动作 y 同步,那么我们也有 hb(x, y)。

这应该足够了。

从哪个版本的Java开始,这将起作用?

Java语言规范,第3版引入了内存模型规范的重写,这是上述保证的关键。NB大多数以前的版本都表现得好像保证存在,许多代码行实际上依赖于它。当人们发现这些担保实际上并不存在时,他们感到惊讶。