论坛首页 Java版

多线程环境下的Observer pattern

浏览 1430 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
最后更新时间:2007-06-03 关键字: Thread
在“The Problem with Threads"论文中提到的用来抨击线程模型的实例。懒得说了,看代码直接:
java 代码
 
  1. public class ValueHolder{  
  2. private List listeners = new LinkedList();  
  3. private int value;  
  4.   
  5. public interface Listener{  
  6. public void valueChanged(int newValue);  
  7. }  
  8.   
  9. public void addListener(Listener listener){  
  10. listeners.add(listener);  
  11. }  
  12.   
  13. public void setValue(int newValue){  
  14. value = newValue;  
  15. Iterator i = copyOfListeners.iterator();  
  16. while(i.hasNext()){  
  17. ((Listener)i.next()).valueChanged(newValue);  
  18. }  
  19. }  
  20. }  
以上代码存在问题是多线程环境下对listeners的竞争访问。既然如此,那在addListener和setValue方法前添加 synchronzied。好了些,不过有死锁的可能,这主要是因为你不知道别人在valueChanged方法中会做什么,在调用这个方法时,你手中已 经紧紧握住一把锁了。继续改进:
java 代码
 
  1. public class ValueHolder{  
  2. private List listeners = new LinkedList();  
  3. private int value;  
  4.   
  5. public interface Listener{  
  6. public void valueChanged(int newValue);  
  7. }  
  8.   
  9. public synchronized void addListener(Listener listener){  
  10. listeners.add(listener);  
  11. }  
  12.   
  13. public void setValue(int newValue){  
  14. List copyOfListeners;  
  15.   
  16. synchronized(this){  
  17. value = newValue;  
  18. copyOfListeners = new LinkedList(listeners);  
  19. }  
  20.   
  21. Iterator i = copyOfListeners.iterator();  
  22. while(i.hasNext()){  
  23. ((Listener)i.next()).valueChanged(newValue);  
  24. }  
  25. }  
  26. }  
此种实现跟JDK源码util包中Observable实现类似,把竞争访问和死锁都排除了。不过此种实现仍存在问题。比如线程A和线程B依次调用 setValue,然后线程B抢在线程A之前通知大家。这样就搞得大家认为ValueHolder中最终value值是线程A设置的值,而实际上是线程B 设置的值。没办法,只能继续改进。
java 代码
 
  1. public class ValueHolder{  
  2. private List listeners = new LinkedList();  
  3. private int value;  
  4. private int seqnum = 0;  
  5. private int globalNum = 1;  
  6.   
  7. public interface Listener{  
  8. public void valueChanged(int newValue);  
  9. }  
  10.   
  11. public synchronized void addListener(Listener listener){  
  12. listeners.add(listener);  
  13. }  
  14.   
  15. public void setValue(int newValue){  
  16. List copyOfListeners;  
  17. int localSeqnum;  
  18.   
  19. synchronized(this){  
  20. value = newValue;  
  21. copyOfListeners = new LinkedList(listeners);  
  22. seqnum++;  
  23. localSeqnum = seqnum;  
  24. }  
  25. while(localSeqnum != globalNum){  
  26. //Only to wait  
  27. }  
  28. Iterator i = copyOfListeners.iterator();  
  29. while(i.hasNext()){  
  30. ((Listener)i.next()).valueChanged(newValue);  
  31. }  
  32. globalNum++;  
  33. }  
  34. }  
以上是我提供的一个实现,不知还有没有问题。关键一点,保证setValue按序执行。

注意:在Java中,局部变量都是线程私有的,不用担心访问冲突,要担心的就是实例变量和类变量。
   
最后更新时间:2007-06-03
应该是没有问题,但不是个好的实现,因为while(localSeqnum != globalNum)太抢CPU时间,在你不知道每个Listener的valueChanged要执行的时间长短时更不能使用这种死循环轮巡的策略。

另外,不管你怎么实现,“线程A和线程B依次调用setValue,然后线程B抢在线程A之前通知大家”肯定是无法避免的,synchronized只能保证互斥,它不保证调用顺序,这完全由操作系统决定。就你给的那个实现,如果线程A在17行时就被操作系统挂起了,让线程B先执行,那结果还是线程B抢在线程A前通知。

所以LZ有点过度设计了,不过如果是学习那倒不是坏事。
   
0 请登录后投票
最后更新时间:2007-06-03
修改为

synchronized(this){
    while(localSeqnum != globalNum){  
        wait();
    }

后面:
synchronized(this){
    globalNum++;
    notifyAll();
}
看这样有没有问题。

这里只是解决了 轮循的问题,不能从根本达到目标,就如楼上所说。
这样还不如简单的把整个方法sync呢。
   
0 请登录后投票
最后更新时间:2007-06-03
Godlikeme 写道

这样还不如简单的把整个方法sync呢。


整个方法括起来是不行的,会有死锁的可能,JDK源码util包中Observable也只是进行block同步。
   
0 请登录后投票
最后更新时间:2007-06-03
也不能将整个setValue方法synchronized掉,因为如果Listener的操作时间很长的话,会阻塞其他线程对其他同步方法的调用,比如说其他线程想添加Listener什么的。所以只要改到第二个实现就够了,再改就过了。
   
0 请登录后投票
最后更新时间:2007-06-03
确实,因为接口的具体实现可能会引起两阶段锁带来的死锁问题,
那就只能如lz把listner.changeValue部分放到synch外面执行,通过一些guard condition来保证执行过程。

但我觉得在这里,方法里synchronized(this) 又没有把this传给listner,应该不会产生这种问题。
哎呀,不行,如果在里面wait()就挂了。
   
0 请登录后投票
最后更新时间:2007-06-03
max.h.chen 写道
应该是没有问题,但不是个好的实现,因为while(localSeqnum != globalNum)太抢CPU时间,在你不知道每个Listener的valueChanged要执行的时间长短时更不能使用这种死循环轮巡的策略。

另外,不管你怎么实现,“线程A和线程B依次调用setValue,然后线程B抢在线程A之前通知大家”肯定是无法避免的,synchronized只能保证互斥,它不保证调用顺序,这完全由操作系统决定。就你给的那个实现,如果线程A在17行时就被操作系统挂起了,让线程B先执行,那结果还是线程B抢在线程A前通知。

所以LZ有点过度设计了,不过如果是学习那倒不是坏事。



这个例子是“The Problem with Threads"论文中提到的,专门用来抨击Thread模型。当然,这个例子应该是作者为了说明Thread模型存在问题而杜撰的,在实际中很难出现:两个线程去更改同一个主题。如果这样设计,那一定是一个糟糕的设计。

这位作者就质疑了作者所举的这个Observer pattern实例,具体请看:http://chaosinmotion.com/blog/?p=26
   
0 请登录后投票
最后更新时间:2007-06-03
max.h.chen 写道
也不能将整个setValue方法synchronized掉,因为如果Listener的操作时间很长的话,会阻塞其他线程对其他同步方法的调用,比如说其他线程想添加Listener什么的。所以只要改到第二个实现就够了,再改就过了。


因为是接口,并不知道实现细节,所以存在这种假设。
那就可以扩展到所有涉及接口的地方都要做这样的假设处理,这就是写多线程非常讨厌的地方。
具体实现很可能不需要这样的处理。

一般情况下,像listener中存在这种响应时间比较长的处理,应该在方法中起一个线程来做,本方法立刻返回。
举一个例子就是eclipse中的编译。
   
0 请登录后投票
最后更新时间:2007-06-03
Godlikeme 写道
确实,因为接口的具体实现可能会引起两阶段锁带来的死锁问题,
那就只能如lz把listner.changeValue部分放到synch外面执行,通过一些guard condition来保证执行过程。

但我觉得在这里,方法里synchronized(this) 又没有把this传给listner,应该不会产生这种问题。


还是有可能的,比如说下面的Listener实现。

class MyListener implements Listener {
	
	private ValueHolder valueHolder;
	
	MyListener(ValueHolder valueHolder) {
		this.valueHolder = valueHolder;
	}
	
	public synchronized void doSomething() {
		synchronized(valueHolder) {
			// do something here
		}
	}
	
	public void valueChanged(int newValue) {
		synchronized (this) {
			synchronized(valueHolder) {
				// do something here
			}
		}
	}
}


如果没有上下文,光看这个MyListener是没有问题的,因为valueChanged和doSomething两个方法使用synchronized的顺序是一样的(MyListener.this -> ValueHolder.this),但是如果放在现在这个上下文里,一旦你将ValueHolder#setValue()整个synchronized掉的话,MyListener#valueChanged获得锁的顺序就变成ValueHolder.this -> MyListener.this -> ValueHolder.this,那就会可能死锁了。
   
0 请登录后投票
最后更新时间:2007-06-03
甚是,甚是。
   
0 请登录后投票
论坛首页 Java版

跳转论坛:
JavaEye推荐