星期日, 六月 03, 2007

多线程环境下的Observer pattern

在“The Problem with Threads"论文中提到的用来抨击线程模型的实例。懒得说了,看代码直接:
public class ValueHolder{
private List listeners = new LinkedList();
private int value;

public interface Listener{
public void valueChanged(int newValue);
}

public void addListener(Listener listener){
listeners.add(listener);
}

public void setValue(int newValue){
value = newValue;
Iterator i = copyOfListeners.iterator();
while(i.hasNext()){
((Listener)i.next()).valueChanged(newValue);
}
}
}
以上代码存在问题是多线程环境下对listeners的竞争访问。既然如此,那在addListener和setValue方法前添加synchronzied。好了些,不过有死锁的可能,这主要是因为你不知道别人在valueChanged方法中会做什么,在调用这个方法时,你手中已经紧紧握住一把锁了。继续改进:
public class ValueHolder{
private List listeners = new LinkedList();
private int value;

public interface Listener{
public void valueChanged(int newValue);
}

public synchronized void addListener(Listener listener){
listeners.add(listener);
}

public void setValue(int newValue){
List copyOfListeners;

synchronized(this){
value = newValue;
copyOfListeners = new LinkedList(listeners);
}

Iterator i = copyOfListeners.iterator();
while(i.hasNext()){
((Listener)i.next()).valueChanged(newValue);
}
}
}
此种实现跟JDK源码util包中Observable实现类似,把竞争访问和死锁都排除了。不过此种实现仍存在问题。比如线程A和线程B依次调用setValue,然后线程B抢在线程A之前通知大家。这样就搞得大家认为ValueHolder中最终value值是线程A设置的值,而实际上是线程B设置的值。没办法,只能继续改进。
public class ValueHolder{
private List listeners = new LinkedList();
private int value;
private int seqnum = 0;
private int globalNum = 1;

public interface Listener{
public void valueChanged(int newValue);
}

public synchronized void addListener(Listener listener){
listeners.add(listener);
}

public void setValue(int newValue){
List copyOfListeners;
int localSeqnum;

synchronized(this){
value = newValue;
copyOfListeners = new LinkedList(listeners);
seqnum++;
localSeqnum = seqnum;
}
while(localSeqnum != globalNum){
//Only to wait
}
Iterator i = copyOfListeners.iterator();
while(i.hasNext()){
((Listener)i.next()).valueChanged(newValue);
}
globalNum++;
}
}
以上是我提供的一个实现,不知还有没有问题。关键一点,保证setValue按序执行。

注意:在Java中,局部变量都是线程私有的,不用担心访问冲突,要担心的就是实例变量和类变量。

星期六, 六月 02, 2007

Thread的层次结构

要弄清楚Thread的层次结构,必须先弄清楚它的参照系。看看“The Futures of Ruby Threading”这段话:“Current stable releases of Ruby use user space threads (also called "green threads"), which means that the Ruby interpreter takes care of everything to do with threads. This is in contrast to kernel threads, where the creation, scheduling and synchronization is done with OS syscalls, which makes these operations costly, at least compared to their equivalents in user space threads.”这里的用户空间线程和内核线程是相对于Ruby解释器来说的。如果把参照系换成操作系统,此处的内核线程只是一个用户空间线程。

常说Java线程是Native thread,是说Java的一个线程映射到操作系统上的一个用户线程。往下随后怎么映射就要看操作系统的实现了。可以看看Solaris的线程模型这里也有些说明资料。

不过Java线程也并不一定是一一映射的,比如Jikes RVM虚拟机,采用了M:N模型,而不是大家经常看到的1:1,具体资料可以看这里。BEA的JRockit是两个都有,既支持1:1模型,也支持M:N模型,叫Thin Thread。

在JVM上实现了M:N模型,当然在操作系统上也可以实现M:N模型,只是层次不同,抽象级别不同。象Solaris就实现了M:N模型,不过这个模型在Solaris 9中已经放弃,为什么?实现太难。“It is not to say that a good implementation of the M:N model is impossible,but simply that a good 1:1 implementation is probably sufficient. ”想更进一步了解Solaris多线程的发展历程,看这个pdf吧(www.sun.com/software/whitepapers/solaris9/multithread.pdf)。

了解清楚Thread的层次结构,碰到Green Thread,Native Thread,User space Thread,Kernel Thread时才不会糊涂。

Thread被过度使用

既然Thread质疑声一片,为什么它仍能够存在呢?原因有:
(1)某些应用程序天生就具有并发特性,而且需要共享地址空间和各种资源,比如数据库服务器。
(2)进程方案开销大。
(3)Java语言大行其道,使得绝大部分程序员认为并发编程唯一也是最好的方式就是采用多线程,而且在Java语言中创建一个线程非常简单。

对于第二点,只有线程创建,线程同步,线程加锁等开销比进程方案开销小,才会真正获益。只是来个线程创建和进程创建开销的比较,那也太天真了。此处,我认为还要包括开发,维护进程并发程序和线程并发程序两种方式之间的开销对比。毕竟,除了机器时间,人的时间也很宝贵。

对于第三点,Java是不是会在以后考虑另外的并发模式。象建立在JVM之上的Scala语言就在采用Actor模式。多个选择,总是不错的主意。不然搞得我们这些开发人员都去用Thread,使得Thread被过度使用,破坏Thread的名声。


附:如果想了解Thread在哪些方面被广泛批评,请参考如下资料:
(1)《The Art of Unix Programming》的“Threads-Threat or Menace?”
(2)“Why Threads Are a Bad Idea”
(3)“The Problem with Threads”