Java语言中内建了对于多线程的支持,可以非常方便的创建、控制线程以及在线程之间进行同步操作。另外,为了支持更为高级的线程间同步机制,比如:类似于POSIX中的条件变量,Java在Object类中提供了wait、notify和notifyAll方法,使得所有的类都隐式的继承了这些方法。特别地,为了提供对于程序健壮性方面的考虑,在Java中提供了对于wait方法超时语意的支持。但是Java在对于wait方法超时语意的支持方面存在模糊性,即在调用具有超时语意的wait方法返回时,无法区分是由于notify的通知还是由于超时触发的。因此应用开发者在构建需要具有超时语意的应用时,就必须负责对这种语意模糊性进行区分,本文将对这一问题进行剖析,并给出一个比较通用的解决方案。
问题提出
在作者所从事的项目中,要开发一个应用服务器,实现如下所述的功能:能够高效的处理来自多个客户端的并发请求。为了简化同步控制,分离并发逻辑和业务逻辑,我们采用了Active Object模式,具体的实现可以参见作者的另外一篇文章:《构建Java并发模型框架》( http://www.ibm.com/developerworks/cn/java/l-multithreading/index.shtml)。该设计中有一个核心部件ActiveQueue用于存放客户的请求。为了能够做到应用服务器的负载控制,我们对于ActiveQueue的大小进行了限制,如果当前的客户请求数量已经达到这个限制,就让后继的请求等待,具体的代码实现片断如下(为了简洁起见,省略了其他无关的代码):
class ActiveQueue {
...
public synchronized void enqueue(ClientRequest cr) throws InterruptedException
{
while(isFull( ) ){ // ActiveQueue的大小达到上限
wait();
}
// 把用户请求添加到处理队列中
notifyAll();
}
...
}
|
该方法刚开始工作的很好,但是随着项目开发的进展,在随后的测试中我们发现了两个较为严重的问题:1、当并发请求的客户端很多时,会造成某些客户端等待的时间过长,对于客户端的使用者来说非常不友好;2、由于系统中应用服务器的其他方面的异常同样会造成客户端请求的永久等待比如:应用服务器在处理完客户端请求后,由于异常没有正确的调用相应notify方法。所以为了改善程序的用户友好性以及健壮性,我们决定采用带有超时语意的wait方法。该方法的原型声明如下:
public final void wait (long millisecTimeout) throws InterruptedException;
使用该方法改进后的代码实现片断如下:
class ActiveQueue {
...
public synchronized void enqueue(ClientRequest cr, long timeout)
throws InterruptedException
{
while(isFull( ) ){ // ActiveQueue的大小达到上限
wait(timeout);
// 语意模糊性体现于此,当wait返回时
// 我们无法区分是由于notify的通知还是超时触发的
// 因此我们无法做出适当的处理
}
// 把用户请求添加到处理队列中
notifyAll();
}
...
}
|
可以看出,简单的使用一个具有超时语意的wait方法是不可行的,原因就在于wait方法超时语意的模糊性。在下面的小节会先给出一个初步的解决方案,随后我们将使用模式对于该方案进行重构从而构造出一个比较通用的方案。
初步解决方案
我们的初步解决方案采用了Doug Lea(Doug Lea为对象并发领域世界级的专家)给出的一个显式的判断算法(关于该算法更加详细、深入的论述请参考参考文献〔1〕),通过该算法来辨别是否已经超时。采用该算法的解决方案的代码实现片断如下:
class ActiveQueue {
...
public synchronized void enqueue(ClientRequest cr, long timeout)
throws InterruptedException, TimeoutException
{
if (isFull ()) { // 判断队列是否为满
long start = System.currentTimeMillis ();
long waitTime = timeout;
for (;;) { // 一直等待到队列不满被notify通知或者超时
wait (waitTime);
if (isFull ()) { //重新判断队列是否为满
long now = System.currentTimeMillis ();
long timeSoFar = now - start; // 队列仍然为满,计算已经等待的时间
if (timeSoFar >= msecTimeout) // 如果超时,抛出TimeoutException异常
throw new TimeoutException ();
else // 没有超时,计算还要等待的时间
waitTime = timeout - timeSoFar;
}
else // 被notify唤醒,并且队列不为满
break;
}
}
// 把用户请求添加到处理队列中
notifyAll();
}
...
}
|
可以看出,这个算法非常的简单,核心思路就是在每次wait返回时,计算wait等待的时间,并比较该时间和设定的要等待的时间,如果大于设定的要等待的时间,即确定为超时,否则确定为被notify唤醒。
使解决方案一般化
上述的解决方案针对我们目前的要求已经可以很好的工作了,但是细心的读者一定会发现在上面给出的解决方案中我们把两个无关的概念揉合在了一起:队列是否为满的判断逻辑和是否超时的计算判断逻辑。为什么说这是两个无关的概念呢?因为队列是否为满是与我们开发的具体应用相关的,不同的应用会有不同类型的判断逻辑(比如:不使用队列的应用可能会有其他在概念上类似的判断逻辑),而计算判断超时的逻辑是和具体应用无关的。如果我们能够把这两个概念剥离开来,那么这二者就可以独立变化,我们的解决方案的可重用性就会增强。
我们使用Template Method模式(参见参考文献〔2〕)来指导我们的重构。首先,我们会根据和具体应用无关的超时计算判断的算法定义一个通用的算法框架,把和具体应用逻辑相关的条件判断作为一个抽象的hook方法,延迟到具体的应用中去实现,从而实现了和应用无关的超时计算判断逻辑的复用。实现该算法的抽象基类的关键实现代码如下:
public abstract class WaitWithTiming
{
// wait方法要作用的对象,对于上述例子就是ActiveQueue
protected Object object_;
public WaitWithTiming (Object obj)
{
object_ = obj;
}
// 这是一个抽象的hook方法,由具体的应用实现,该方法由本算法框架调用
public abstract boolean condition ();
// 计算判断超时的算法框架实现
public final void timedWait (long timeout)
throws InterruptedException, TimeoutException
{
if (condition ()) { //调用具体应用实现的hook方法
long start = System.currentTimeMillis ();
long waitTime = msecTimeout;
for (;;) {
object_.wait (waitTime);
if (condition ()) {
long now = System.currentTimeMillis ();
long timeSoFar = now - start;
if (timeSoFar >= msecTimeout)
throw new TimeoutException ();
else
waitTime = timeout - timeSoFar;
}
else
break;
}
}
}
public final void announce() {
object_.notifyAll ();
}
}
|
使用方法介绍
本小节我们将对上一节给出的抽象基类WaitWithTiming的使用方法进行详细的介绍。我们当然可以直接使得ActiveQueue继承自WaitWithTiming,并实现相应的抽象hook方法condition,但是这样做有一个弊端,就是对于ActiveQueue我们只能够实现仅仅一个condition,如果我们要添加针对dequeue时队列为空的条件判断逻辑就无能为力了,因为WaitWithWaiting仅仅只有一个condition方法(其实,即使有多个也没有办法做到通用,因为不能对具体的应用的需求进行假设)。
我们推荐的使用方法是,根据具体应用的需求,整理出需要的判断条件,创建相应的类来表示这些判断条件,使这些用来表示具体判断条件的类继承自WaitWithTiming,这些类中具体的条件判断逻辑的实现可以使用相应的具体的应用实体。比如:对于本文开始所列举的应用,我们需要的判断条件为队列为满,所以我们可以定义一个QueueFullCondition类继承自WaitWithTiming,在QueueFullCondition中实现抽象的hook方法condition的逻辑,在该逻辑中在使用ActiveQueue的isFull方法。使用这种委托的方法,我们就可以比较有效的解决一个对象同时需要多个判断条件的问题(不同的判断条件只需定义不同的子类即可)。相应的UML结构图和关键代码实现如下:
UML结构图:
关键代码片断:
class QueueFullCondition extends WaitWithTiming
{
public QueueFullCondition (ActiveQueue aq)
{ super (aq); } // 为WaitWithTiming中的object_赋值
public boolean condition () {
ActiveQueue aq = (ActiveQueue) object_; //使用ActiveQueue来实现具体的判断逻辑
return aq.isFull ();
}
}
class ActiveQueue {
...
public synchronized void enqueue(ClientRequest cr, long timeout)
throws InterruptedException, TimeoutException
{
//具有时限控制的等待
queueFullCondition_.timedWait (timeout);
// 把用户请求添加进队列
//唤醒等待在ActiveQueue上的线程
queueFullCondition_.announce ();
}
...
private QueueFullCondition queueFullCondition_ = new QueueFullCondition (this);
}
|
要注意的问题
如果读者朋友仔细观察的话,就会觉察到在WaitWithTiming类中的timedWait方法的定义中没有添加synchronized关键字,这一点是非常关键的,因为是为了避免在编写并发的Java应用时一个常见的死锁问题:嵌套的monitor。下面对于这个问题进行简单的介绍,关于这一问题更为详细的论述请参见参考文献〔1〕。
什么是嵌套的monitor问题呢?嵌套的monitor是指:当一个线程获得了对象A的monitor锁,接着又获得了对象B的monitor锁,在还没有释放对象B的monitor锁时,调用了对象B的wait方法,此时,该线程释放对象B的monitor锁并等待在对象B的线程等待队列上,但是此时该线程还拥有对象A的monitor锁。如果该线程的唤起条件依赖于另一个线程首先要获得对象A的monitor锁的话,就会引起死锁,因为此时别的线程无法获得上述线程还没有释放的对象A的monitor锁,结果就出现了死锁情况。一般的解决方案是:在设计时线程不要获取对象B的monitor锁,而仅仅使用对象A的monitor锁。
针对我们前面列举的例子,ActiveQueu可以类比为对象A,queueFullContion_可以类比为对象B,如果我们在timedWait方法前面添加上synchronized关键字,就有可能会发生上述的死锁情况,因为当我们在调用ActiveQueu的enqueue方法中调用了queueFullContion_的timedWait方法后,如果队列为满,虽然我们释放了queueFullContion_的monitor锁,但是我们还持有ActiveQueue的monitor锁,并且我们的唤醒条件依赖于另外一个线程调用ActivcQueue的dequeue方法,但是因为此时我们没有释放ActiveQueue的monitor锁,所以另外的线程就无法调用ActiveQueu的dequeue方法,那么结果就是这两个线程就都只能够等待。
总结
本文对于Java中wait方法超时语意的模糊性进行了分析,并给出了一个比较通用的解决方案,本解决方案对于需要精确的超时语意的应用还是无法很好的适用的,因为方案中所给出的关于超时计算的算法是不精确的。还有一点就是有关嵌套monitor的问题,在编写多线程的Java程序时一定要特别注意,否则非常容易引起死锁。其实,本文所讲述的所有问题的根源都是由于Java对于wait方法超时语意实现的模糊性造成的,如果在后续的Java版本中对此进行了修正,那么本文给出的解决方案就是多余的了。
posted on 2009-05-19 14:17
郭鹏 阅读(194)
评论(0) 编辑 收藏 所属分类:
JAVA