【问题标题】:How a thread can see stale reference of safely initialized object线程如何看到安全初始化对象的过时引用
【发布时间】:2019-01-27 08:33:55
【问题描述】:

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

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 类中。

【问题讨论】:

  • 不清楚你在这里问什么。 helper 未安全发布。因此,您需要安全地发布它。
  • @AndyTurner 我是否认为不可变对象可以在不进行额外同步的情况下安全发布的说法不正确?
  • @RubaMushtaq 您不发布对象,而是发布对对象的引用。如果您分发的引用不是最新的,则对象是否安全初始化无关紧要。
  • @AndyTurner 感谢您提及更新参考。

标签: java multithreading thread-safety


【解决方案1】:

您发布对不可变对象的引用这一事实与此无关。

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

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

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

确保写入发生在读取之前的最简单方法是使helper成员变量final:写入final字段的值保证在构造函数结束之前发生,所以所有线程总是能看到该字段的正确值(前提是 this 没有在构造函数中泄露)。

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

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

因此,您必须自己创建这种先发生的关系。三种机制是:

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

【讨论】:

  • 第三个选项可能是AtomicReference<Helper>。与AtomicInteger 的推理相同(但对于确实需要Helper 的情况)。
  • @AndyTurner 我可以说一个安全发布的对象应该通过适当的机制正确构建,即最终或同步,否则它可能有陈旧的数据。
【解决方案2】:

引用Volatile vs Static in Java

这意味着如果两个线程同时更新同一 Object 的变量,并且该变量未声明为 volatile,则可能存在其中一个线程缓存旧值的情况。

根据您的代码,可能会发生以下情况:

  • 线程 1 调用 getHelper() 并获得 null
  • 线程 2 调用 getHelper() 并获得 null
  • 线程 1 调用 setHelper(42)
  • 线程 2 调用 setHelper(24)

在这种情况下,您的麻烦开始于哪个 Helper 对象将在哪个线程中使用。关键字volatile 至少可以解决缓存问题。

【讨论】:

  • 这意味着这是一个缓存未部分初始化的对象的问题。在这种情况下,对象总是会被安全地初始化,但引用可能是陈旧的。
【解决方案3】:

变量helper 正在被多个线程同时读取。至少,您必须将其设为volatile,否则编译器将开始将其缓存在线程本地的寄存器中,并且对变量的任何更新都可能不会反映在主内存中。使用volatile,当线程开始读取共享变量时,它将清除其缓存并从全局内存中获取新值。当它读完它时,它会将其缓存的内容刷新到主内存中,以便其他线程可以获取更新的值。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多