2011-07-24 43 views
2

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

可以说我有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); 
    } 
} 

所以线程1将更新缓存的状态,然后线程2做同样的,它会覆盖状态(记不采取从线程1中更新的状态)

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

所以我不明白何时使用不可变对象?这是否取决于如果我在使用最新状态时看到并且不需要更新状态的过程中同时进行修改时是否可以正常工作?

+0

这是Java,我假设? – skaffman

+1

'MyImmutableState state'必须是易失性的。 – bestsss

回答

4

出版似乎是有点棘手的概念,它是在Java并发实践解释的方式并没有很好地工作,我(相对于许多其他多线程的概念,这wonderful book解释)。

考虑到上述问题,我们首先要清楚问题的一些简单部分。

  • 当你的状态可以说线程1完成第一

    - 你怎么会知道?或者,更准确地说,thread2“知道”那个?据我可以告诉这可能只能用某种同步,明确或不那么明确,如线程加入(请参阅JLS - 17.4.5 Happens-before Order)。你的代码提供迄今没有给出足够的细节来讲述这个是否属实与否

  • ,当你指出线程1将更新缓存的状态 - 怎么会线程2“知道”?同一段代码,你提供的,它看起来完全有可能(但不能保证你要知道)为线程2永远不会知道这个更新

  • 当你的状态线程2 ......将覆盖状态什么呢覆盖是什么意思?GlobalStateCache没有什么代码示例,可以以某种方式保证线程1将永远不会注意到这个覆盖。更多的是,代码提供的暗示没有什么会以某种方式强制来自不同线程的更新happen-before relation,所以人们甚至可以推测覆盖可能发生相反的情况,你看?

  • 最后但并非最不重要的措辞不变的状态听起来对我来说很模糊。鉴于这个棘手的问题,我会说很危险。该字段状态是可变的,它可以通过调用方法更新状态对吗?从你的代码我宁愿得出结论认为,MyImmutableState类的实例被认为是不可变的 - 至少这是什么名字告诉我。

与所有上面说的,什么是保证可见你至今所提供的代码?我害怕的不多,但也许总比没有好。我看事情是这样的......

对于线程1,可以保证调用updateState之前,它会看到无论是或适当建造从线程2更新(有效)对象。更新之后,它将保证看到从thread1或thread2更新的正确构造的对象(有效的)。在此更新之后请注意,thread1保证不会看到null每个JLS 17.4.5我参考上面(“... x和y是同一个线程的动作,x在程序顺序中出现在y之前... “

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

从本质上讲,是与您所提供的代码,保证全部是这两个线程会看到无论是或构造的一个正常MyImmutableState类(有效)实例。

以上保证乍一看可能看起来微不足道,但是如果您在引用之前浏览了一页以上的内容(“不可变对象可以安全使用等)”,您会发现一个值得称赞的例子深入钻探进入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."); 
    } 
} 

戈茨上述代码的注释开始解释可变和不可变对象的真实问题,

...我们说持有人是 未正确发布。有两件事情可能因错误发布的对象而出错。其他线程可能会看到持有者字段的陈旧值,因此即使已将值放入持有者中,也会看到空引用或其他较旧的值。

...然后他潜入如果对象是可变

...... 可是差远了,其他线程可以看到持有人参考的上行TODATE价值,但对于陈旧的价值观会发生什么持有人的状态。为了使 的可预测性更差,线程在第一次读取字段时可能会看到一个陈旧值,并在下次更新该值时,这就是为什么assertSanity可能会抛出 AssertionError

以上“AssertionHorror”听起来可能违反直觉的,但所有的魔法消失,如果你认为情况如下图所示(每个Java 5的内存模型完全合法的 - 和BTW一个很好的理由):

  1. thread1调用sharedHolderReference = Holder(42);

  2. 线程1首先填充Ñ字段具有默认值(0),那么将要构造内分配,但...

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

  4. sharedHolderReference from thread1对thread2变得可见,因为,说因为为什么不呢?也许优化热点编译器决定它是该

  5. 的好时机线程2读取上TODATE sharedHolderReference与字段值仍然为0 BTW

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

  7. thread2读取的左侧值,如果语句在assertSanity这是,那么0,那么它将读取右侧val UE但...

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

  9. 线程1通过设置Ñ字段值42

  10. 完成悬浮在上述步骤#2的构造的分配

    值42在该领域n从thread1变为对thread2可见,因为,说因为为什么不呢?也许优化热点编译器决定了它是该

  11. 良好的时间的话,在某个时刻以后,调度切换回从线程2轮

  12. 线程2继续进行,在其中在步骤#6悬浮以上,即它读取如果声明,这是,好了,42现在

  13. 哎呀我们无辜如果右侧(N!= N)突然变成如果(0!= 42)这...

  14. ...自然抛出Asse田

据我了解,对于不可变对象初始化安全只是保证上面不会发生 - 没有更多...也不能少

+0

我不明白为什么step4会发生。为什么在对象的构造函数返回之前将引用分配给对象? –

+0

@AdrianLiu,因为它不被禁止。如果代码不同步,则允许这样做。如果没有理由等待,为何等待 – gnat

+0

我认为对于单线程程序来说这种行为不可能更快;对于多线程程序,编译器的这种行为会产生部分初始化问题。我错了吗? –

1

如果我理解你的问题,不变性在这里似乎并不相关。你只是问线程是否会看到共享对象的更新。在交换评论之后,我现在看到您还需要在执行某些操作时持有对共享单体状态的引用,然后设置状态以反映该操作。

好消息,和以前一样,提供这种必然性也解决了你的记忆一致性问题。

而不是定义单独同步getStateupdateState方法,你就必须不中断执行三个动作:getStateyourActionupdateState

我可以看到三种方式来做到这一点:

1)不要在GlobalStateCache一个synchronized方法内的所有三个步骤。在GlobalStateCache中定义一个原子doActionAndUpdateState方法,当然在你的state单例中同步,这将需要一个函子对象来执行你的动作。

2)待办事项getStateupdateState作为单独的呼叫,并改变updateState,以便它检查,以确保状态,因为GET并没有改变。在GlobalStateCache中定义getStatecheckAndUpdateStatecheckAndUpdateState将接收从getState获得的原始状态的来电者,并且必须能够检查自从您获得状态后状态是否已更改。如果已更改,则需要采取措施让呼叫者知道他们可能需要恢复其操作(取决于您的使用情况)。

3)在GlobalStateCache中定义getStateWithLock方法。这意味着你还需要确保呼叫者释放他们的锁定。我会创建一个明确的releaseStateLock方法,并让您的updateState方法调用它。

其中,我建议不要使用#3,因为这会让您容易在某些类型的错误中锁定状态。我还建议(虽然不太强烈)反对#2,因为它会在状态发生变化时发生的事情变得复杂:您是否放弃了行动?你会重试吗?它必须是(可以)还原吗?我对#1:单原子同步方法,这将是这个样子:

public interface DimitarActionFunctor { 
    public void performAction(); 
} 
GlobalStateCache {  
    private MyImmutableState state = MyImmutableState.newInstance(null, null);  
    public MyImmutableState getState {  
     synchronized(state) { 
     return state;  
     } 
    }  
    public void doActionAndUpdateState(DimitarActionFunctor functor, State currentState, Args newArgs){  
     synchronized(state) { 
     functor.performAction(); 
     state = MyImmutableState.newInstance(currentState, newArgs);  
     } 
    } 
    } 
} 

来电然后构造的动作算符(DimitarActionFunctor的实例),并调用doActionAndUpdateState。当然,如果动作需要数据,你必须定义你的函数接口来把这些数据作为参数。

同样,我点你这个问题,而不是实际的差异,但对于他们如何在内存一致性方面都工作:Difference between volatile and synchronized in Java

+0

感谢您的回答,我知道有什么区别。 我知道确保不一致状态的最好方法是同步或原子引用。但问题是不同的。 这些书说:“不可变对象可以被任何线程安全地使用,而不需要额外的同步,即使在没有使用同步来发布它们时也是如此。” – dimitar

+0

不可变对象始终是安全发布的。我想知道如果线程修改状态,在另一个线程开始与状态一起工作之后会发生什么。 – dimitar

+0

如果您同步所有可以更改状态的方法,则不会出现两个线程一次试图修改状态的问题。如果你的用例读起来像“获取当前状态,执行操作,修改它”,那么线程必须得到状态,执行它们的操作,然后执行原子get-check-and-set,你必须写。 – CPerkins

0

这在很大程度上取决于实际使用的情况下,在这里,它很难做一个推荐,但是看起来你需要一些使用java.util.concurrent.atomic.AtomicReference的GlobalStateCache的Compare-And-Set语义。

public class GlobalStateCache { 
    AtomicReference<MyImmutableState> atomic = new AtomicReference<MyImmutableState>(MyImmutableState.newInstance(null, null); 

    public State getState() 
    { 
     return atomic.get(); 
    } 

    public void updateState(State currentState, Args newArgs) 
    { 
     State s = currentState; 
     while (!atomic.compareAndSet(s, MyImmutableState.newInstance(s, newArgs))) 
     { 
      s = atomic.get(); 
     } 
    } 
} 

这当然取决于有可能造成一些额外的MyImmutableState对象的费用,以及是否需要重新运行myMethod的(状态),如果状态已经下更新,但这个概念应该是正确。

+0

我知道这种方法,它看起来非常像“乐观锁定”,你乐观地认为现在状态会改变,如果是,那么你重新尝试一下这个动作。 – dimitar

+0

循环完全没用,如果你没有有意义的谓词/ if条件CAS没有意义 – bestsss

1

我认为关键是区分对象和引用。

不可变对象是安全发布的,所以任何线程都可以发布对象,并且如果任何其他线程读取对这样的对象的引用 - 它可以安全地使用该对象。当然,读线程会看到线程在读取引用时发布的不可变对象状态,直到它再次读取引用时才会看到任何更新。

它在许多情况下非常有用。例如。如果只有一个出版商,并且许多读者 - 读者需要看到一致的状态。读者定期阅读参考资料,并处理获得的状态 - 保证一致,并且不需要对读取器线程进行任何锁定。另外,当可以松开一些更新时,例如你不关心哪个线程更新状态。

0

回答你“主”的问题:没有线程2不会看到变化。不变的对象不会改变:-)

因此,如果Thread1读取状态A,然后Thread2存储状态B,则Thread1应该再次读取变量以查看更改。

变量的Visibily受关键字volatile影响。如果变量声明为volatile那么Java保证如果一个线程更新变量,所有其他线程将立即看到变化(以速度为代价)。

仍然不可变的对象在多线程环境中非常有用。我会给你一个例子,我如何使用它一次。比方说,你有一个对象定期更改(在我的情况下,life字段)由一个线程,它由某种方式由其他线程处理(我的程序通过网络发送给客户端)。如果对象在处理过程中发生更改(它们发送不一致的生命字段状态),则这些线程会失败。如果你让这个对象不可改变并且每次改变都会创建一个新的实例,那么你根本不需要写任何同步。更新线程将定期发布对象的新版本,并且每当其他线程读取它时,它们都将具有最新版本并可以安全地处理它。这个特殊的例子节省了花在同步上的时间,但浪费了更多的内存这应该给你一个一般的理解,当你可以使用它们。

另一个链接我发现:http://download.oracle.com/javase/tutorial/essential/concurrency/immutable.html

编辑(回答评论):

我会解释我的任务。我必须编写一个网络服务器,它将向客户发送最近的生活领域,并不断更新它。使用上述设计,我有两种类型的线程:

  1. 更新表示生命字段的对象的线程。它是不可变的,所以实际上它每次都会创建一个新实例,只是改变引用。引用被声明为volatile的事实至关重要。
  2. 每个客户都有自己的线程。当客户端请求生命字段时,它会读取参考一次并开始发送。由于网络连接速度较慢,因此该线程发送数据时,可以多次更新生命字段。对象不可变的事实保证服务器将发送一致的生命状态字段。在注释中,您关心此线程处理对象时所做的更改。是的,当客户收到数据时,它可能不是最新的,但你无能为力。这不是同步问题,而是一个缓慢的连接问题。

我是不是指出不可变对象可以解决所有的并发问题。这显然不是真的,你指出了这一点。我试图解释你在哪里,实际上可以通过解决问题。我希望我的例子现在很清楚。

+0

volataile是轻量级的同步引用java规范在发布后不保证可见性,只有线程会使用最新值。在你的情况下,如果发送了两个不可变对象r,但是较新的获得第一个,然后第二个覆盖它,那么会发生什么 – dimitar

+0

即使状态是同步的,如果你发布它不会工作 – dimitar

+0

@dimitar我明白你不相信我,在这里是[证明](http://java.sun.com/docs/books/jls/third_edition/html/classes.html#8.3.1.4)。特别是:一个字段可能被声明为volatile,在这种情况下,Java内存模型(§17)确保所有线程都看到变量的一致值。并且:写一个易失性字段(§8.3.1.4)发生在每个后续读取该字段之前。 – Thresh