线程如何看到安全初始化对象的陈旧引用

时间:2019-01-27 08:33:56

标签: java multithreading thread-safety

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

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

3 个答案:

答案 0 :(得分:2)

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

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

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

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

确保写入发生在读取之前的最简单方法是使helper成员变量final:对final字段值的写入保证在发生之前发生构造函数的末尾,因此所有线程总是看到正确的字段值(假设this没有在构造函数中泄漏)。

显然final在这里不是一种选择,因为您有二传手。因此,您必须采用其他机制。

以代码的票面价值为准,最简单的选择是使用{final} AtomicInteger而不是Helper类:对AtomicInteger的写保证在后续读取之前发生。但是我想您实际的助手类可能会更复杂。

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

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

答案 1 :(得分:1)

引用Volatile vs Static in Java

  

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

给出您的代码,可能会发生以下情况:

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

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

答案 2 :(得分:0)

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