首页 > 解决方案 > 线程如何看到安全初始化对象的过时引用

问题描述

我一直在试图弄清楚如何通过陈旧的引用观察到安全发布的不可变对象。

public final class Helper {
private final int n;

  public Helper(int n) {
    this.n = n;
  }
} 

class Foo {
  private Helper helper;

  public Helper getHelper() {
    return helper;
  }

  public void setHelper(int num) {
    helper = new Helper(num);
  }
} 

到目前为止,我可以理解 Helper 是不可变的并且可以安全地发布。读取线程读取 null 或完全初始化的 Helper 对象,因为它在完全构造之前不可用。解决方案是将 volatile 放在我不明白的 Foo 类中。

标签: javamultithreadingthread-safety

解决方案


您发布对不可变对象的引用这一事实在这里无关紧要。

如果您正在从多个线程读取引用的值,如果您关心使用最新值的所有线程,则需要确保写入发生在读取之前。

Happens before是语言规范中精确定义的术语,特别是关于 Java 内存模型的部分,它允许线程进行优化,例如通过不总是更新主内存中的内容(这很慢),而是将它们保存在本地缓存(这要快得多,但可能导致线程为“相同”变量保存不同的值)。Happens-before 是一种关系,可帮助您推理在使用这些优化时多个线程如何交互。

除非您实际上创建了先发生关系,否则无法保证您会看到最新的值。在您显示的代码中, 的写入和读取之间没有这种关系helper,因此您的线程不能保证看到 的“新”值helper。他们可能会,但他们可能不会。

确保写入发生在读取之前的最简单方法是创建helper成员变量final:对字段值的写入final保证在构造函数结束之前发生,因此所有线程始终看到字段的正确值(提供this的没有在构造函数中泄漏)。

显然,在这里制作它final不是一个选项,因为你有一个二传手。所以你必须使用一些其他的机制。

从表面上看代码,最简单的选择是使用(final)AtomicInteger而不是Helper类:写入AtomicInteger保证在后续读取之前发生。但我猜你的实际助手类可能更复杂。

所以,你必须自己创造这种事前发生的关系。三个机制是:

  • 使用AtomicReference<Helper>: 这与 具有相似的语义AtomicInteger,但允许您存储引用类型的值。(感谢您指出这一点,@Thilo)。
  • 制作字段volatile:这保证了最近写入的值的可见性,因为它会导致写入刷新到主内存(而不是从线程的缓存中读取),并从主内存中读取。它有效地阻止了 JVM 进行这种特定的优化。
  • 访问同步块中的字段。最简单的做法是使 getter 和 setter 方法同步。值得注意的是,您不应该在 上同步helper,因为该字段正在更改。

推荐阅读