发布后不可变对象的可见性

dim*_*tar 2 java multithreading immutability

我有一个不变的对象,它封装在类中并且是全局状态。

可以说我有2个线程获得此状态,并执行myMethod(state)。并说thread1首先完成。它修改全局状态,调用GlobalStateCache.updateState(state,newArgs);。

GlobalStateCache {
   MyImmutableState state = MyImmutableState.newInstance(null, null);

   public void updateState(State currentState, Args newArgs){
      state = MyImmutableState.newInstance(currentState, newArgs);
   }
}
Run Code Online (Sandbox Code Playgroud)

因此,线程1将更新缓存的状态,然后线程2进行相同的操作,它将覆盖该状态(请注意,从线程1更新的状态)

我搜索了Google,Java规范并在实践中阅读了Java并发性,但是显然没有指定。我的主要问题是,对已经读取了不可变状态的线程来说,不可变状态对象的值是否可见。我认为它不会看到更改后的状态,只有在更新后才能看到它。

所以我不明白何时使用不可变对象?这是否取决于我在我看到的最新状态下是否可以进行并发修改并且不需要更新状态?

gna*_*nat 5

发布似乎是一个棘手的概念,而且在实践中用Java并发解释它的方式对我来说不太好(与这本出色的书中解释的许多其他多线程概念相反)。

考虑到以上几点,让我们首先弄清楚问题的一些简单部分。

  • 当您声明说thread1首先完成时 -您怎么知道的?或者,更确切地说,thread2如何 “知道”这一点?据我所知,只有通过某种同步,显式或不太明显的同步(如线程连接),才有可能实现(请参阅JLS- 17.4.5在订单发生之前)。到目前为止,您提供的代码没有提供足够的详细信息来说明是否是这种情况

  • 当您声明线程1将更新缓存状态时 - 线程2如何“知道”该状态?使用您提供的代码,thread2看起来完全有可能(但不能保证,请记住),永远不会知道此更新

  • 当您声明thread2 ...将覆盖状态时,这里的覆盖意味着什么?GlobalStateCache代码示例中没有什么可以以某种方式保证thread1会注意到此重写。更甚者,所提供的代码不同线程之间的更新关系之前,并没有暗示任何会强加发生的事情,因此您甚至可以推测重写可能会反过来发生,您知道吗?

  • 最后但并非最不重要的一点是,对于我来说,不变状态的措辞听起来相当模糊。考虑到这个棘手的话题,我会说危险的模糊。字段状态是可变的,可以通过调用方法updateState来更改它,对吗?从您的代码中,我想得出一个结论:假定MyImmutableState类的实例是不可变的-至少那是名字告诉我的。

综上所述,到目前为止,您所提供的代码保证可以看见什么?恐怕不多,但也许总比没有好。我的看法是...

对于thread1,可以确保在调用updateState之前,它将看到null或从thread2更新的正确构造的(有效)对象。更新之后,可以确保看到从线程1或线程2更新的正确构造的(有效)对象。请注意,在此更新线程1之后,按照我上面提到的JLS 17.4.5,保证不会看到null“ ... x和y是同一线程的操作,并且x在程序顺序中位于y之前”

对于thread2,保证与上面的非常相似。

本质上,您提供的代码可以保证的是,两个线程都将看到nullMyImmutableState类的正确构造(有效)实例之一。

乍一看,上述保证看起来微不足道,但是如果您在报价单上略过一页(令您感到困惑的话)(“不可变的对象可以安全使用,等等...”),您会发现一个值得深入研究的示例。3.5.1。发布不当:当好对象变坏时

是的,仅是不可变的对象并不能保证其可见性,但至少可以保证该对象不会“从内部爆炸”,如3.5.1中提供的示例所示:

public class Holder {
  private int n;

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

  public void assertSanity() {
    if (n != n)
      throw new AssertionError("This statement is false.");
  }
}
Run Code Online (Sandbox Code Playgroud)

上面的代码对Goetz的注释从解释可变对象和不可变对象的真实问题开始,

...我们说Holder 没有正确发布。不正确发布的对象可能会导致两件事。其他线程可能会在holder字段中看到一个过时的值,因此即使将值放置在holder中,也会看到一个空引用或其他较旧的值...

...然后他潜入对象可变的情况下可能发生的情况,

... 但更糟糕的是,其他线程可能会看到Holder引用的最新值,但是对于Holder的状态却是陈旧的值。为了使事情变得更不可预测,线程在第一次读取字段时可能会看到过时的值,而在下一次读取时可能会看到最新的值,这就是assertSanity可以引发AssertionError的原因

上面的“ AssertionHorror”听起来似乎违反直觉,但是如果您考虑以下情况(所有Java 5内存模型完全合法-顺便说一句),所有的魔力就消失了:

  1. 线程1调用sharedHolderReference = Holder(42);

  2. thread1首先用默认值(0)填充n字段,然后在构造函数中分配它,但是...

  3. ...但是调度程序切换到线程2,

  4. 线程 2可以看到来自线程1的sharedHolderReference,因为为什么不这样做?也许优化热点编译器决定现在是个好时机

  5. thread2读取最新的sharedHolderReference,其字段值仍为0 btw

  6. 线程2调用sharedHolderReference.assertSanity()

  7. 线程2读取的左侧值,如果内声明assertSanity是,好了,0则它会读取右边的值,但...

  8. ...但是调度程序切换回线程1,

  9. 线程1通过设置n字段值42 来完成在上面的步骤2中挂起的构造函数分配

  10. 线程2可以看到线程1 的字段n中的值42,这是因为(为什么)为什么?也许优化热点编译器决定现在是个好时机

  11. 然后,稍后,调度程序切换回线程2

  12. 线程2从上面步骤6中挂起的位置继续进行,即它读取if语句的右侧,现在是42

  13. 如果(n!= n)突然变成if(0!= 42),我们无辜了...

  14. ...自然会引发AssertionError

据我了解,不可变对象的初始化安全只是保证不会发生以上情况-不多...也不少