关于解决 Java 编程语言线程问题的建议(2)

发表于:2007-07-14来源:作者:点击数: 标签:
在《Taming Java Threads》的第八章中,我给出了一个 服务器 端的 socket 处理程序,作为线程池的例子。它是关于使用线程池的任务的一个好例子。其基本思路是产生一个独立对象,它的任务是监控一个服务器端的 socket。每当一个客户机连接到服务器时,服务器
在《Taming Java Threads》的第八章中,我给出了一个服务器端的 socket 处理程序,作为线程池的例子。它是关于使用线程池的任务的一个好例子。其基本思路是产生一个独立对象,它的任务是监控一个服务器端的 socket。每当一个客户机连接到服务器时,服务器端的对象会从池中抓取一个预先创建的睡眠线程,并把此线程设置为服务于客户端连接。socket 服务器会产出一个额外的客户服务线程,但是当连接关闭时,这些额外的线程将被删除。实现 socket 服务器的推荐语法如下:
public $pooled(10) $task Client_handler
{
PrintWriter log = new PrintWriter( System.out );

public asynchronous void handle( Socket connection_to_the_client )
{
log.println("writing");

// client-handling code goes here. Every call to
// handle() is executed on its own thread, but 10
// threads are pre-created for this purpose. Additional
// threads are created on an as-needed basis, but are
// discarded when handle() returns.
}
}

$task Socket_server
{
ServerSocket server;
Client_handler client_handlers = new Client_handler();

public Socket_server( int port_number )
{ server = new ServerSocket(port_number);
}

public $asynchronous listen(Client_handler client)
{
// This method is executed on its own thread.

while( true )
{ client_handlers.handle( server.aclearcase/" target="_blank" >ccept() );
}
}
}

//...

Socket_server = new Socket_server( the_port_number );
server.listen()




Socket_server 对象使用一个独立的后台线程处理异步的 listen() 请求,它封装 socket 的“接受”循环。当每个客户端连接时,listen()
请求一个 Client_handler 通过调用 handle() 来处理请求。每个 handle() 请求在它们自己的线程中执行(因为这是一个 $pooled 任务)。




注意,每个传送到 $pooled $task 的异步消息实际上都使用它们自己的线程来处理。典型情况下,由于一个
$pooled $task 用于实现一个自主操作;所以对于解决与访问状态变量有关的潜在的同步问题,最好的解决方法是在
$asynchronous 方法中使用 this 是指向的对象的一个独有副本。这就是说,当向一个
$pooled $task 发送一个异步请求时,将执行一个 clone() 操作,并且此方法的
this 指针会指向此克隆对象。线程之间的通信可通过对 static 区的同步访问实现。

改进 synchronized

虽然在多数情况下, $task 消除了同步操作的要求,但是不是所有的多线程系统都用任务来实现。所以,还需要改进现有的线程模块。
synchronized 关键字有下列缺点:


无法指定一个超时值。

无法中断一个正在等待请求锁的线程。

无法安全地请求多个锁 。(多个锁只能以依次序获得。)


解决这些问题的办法是:扩展 synchronized 的语法,使它支持多个参数和能接受一个超时说明(在下面的括弧中指定)。下面是我希望的语法:


synchronized(x && y && z)
获得 x、y 和 z
对象的锁。



synchronized(x || y || z)
获得 x、y 或 z
对象的锁。







synchronized( (x && y ) || z)
对于前面代码的一些扩展。



synchronized(...)[1000]
设置 1 秒超时以获得一个锁。



synchronized[1000] f(){...}
在进入 f() 函数时获得 this 的锁,但可有 1 秒超时。





TimeoutException 是 RuntimeException 派生类,它在等待超时后即被抛出。

超时是需要的,但还不足以使代码强壮。您还需要具备从外部中止请求锁等待的能力。所以,当向一个等待锁的线程传送一个
interrupt() 方法后,此方法应抛出一个 SynchronizationException 对象,并中断等待的线程。这个异常应是 RuntimeException
的一个派生类,这样不必特别处理它。

对 synchronized 语法这些推荐的更改方法的主要问题是,它们需要在二进制代码级上修改。而目前这些代码使用进入监控(enter-monitor)和退出监控(exit-monitor)指令来实现
synchronized。而这些指令没有参数,所以需要扩展二进制代码的定义以支持多个锁定请求。但是这种修改不会比在
Java 2 中修改 Java 虚拟机的更轻松,但它是向下兼容现存的 Java 代码。

另一个可解决的问题是最常见的死锁情况,在这种情况下,两个线程都在等待对方完成某个操作。设想下面的一个例子(假设的):


class Broken
{ Object lock1 = new Object();
Object lock2 = new Object();

void a()
{ synchronized( lock1 )
{ synchronized( lock2 )
{ // do something
}
}
}

void b()
{ synchronized( lock2 )
{ synchronized( lock1 )
{ // do something
}
}
}




设想一个线程调用 a(),但在获得 lock1之后在获得 lock2 之前被剥夺运行权。
第二个线程进入运行,调用 b(),获得了 lock2,但是由于第一个线程占用 lock1,所以它无法获得
lock1,所以它随后处于等待状态。此时第一个线程被唤醒,它试图获得 lock2,但是由于被第二个线程占据,所以无法获得。
此时出现死锁。下面的 synchronize-on-multiple-objects 的语法可解决这个问题:




//...
void a()
{ synchronized( lock1 && lock2 )
{
}
}

void b()
{ synchronized( lock2 && lock3 )
{
}
}





编译器(或虚拟机)会重新排列请求锁的顺序,使 lock1 总是被首先获得,这就消除了死锁。




但是,这种方法对多线程不一定总成功,所以得提供一些方法来自动打破死锁。一个简单的办法就是在等待第二个锁时常释放已获得的锁。这就是说,应采取如下的等待方式,而不是永远等待:




while( true )
{ try
{ synchronized( some_lock )[10]
{ // do the work here.
break;
}
}
catch( TimeoutException e )
{ continue;
}
}




如果等待锁的每个程序使用不同的超时值,就可打破死锁而其中一个线程就可运行。我建议用以下的语法来取代前面的代码:




synchronized( some_lock )[]
{ // do the work here.
}




synchronized 语句将永远等待,但是它时常会放弃已获得的锁以打破潜在的死锁可能。在理想情况下,每个重复等待的超时值比前一个相差一随机值。


改进 wait() 和 notify()


wait()/notify() 系统也有一些问题:


无法检测 wait() 是正常返回还是因超时返回。

无法使用传统条件变量来实现处于一个“信号”(signaled)状态。

太容易发生嵌套的监控(monitor)锁定。

超时检测问题可以通过重新定义 wait() 使它返回一个
boolean 变量 (而不是 void ) 来解决。一个 true
返回值指示一个正常返回,而 false 指示因超时返回。


基于状态的条件变量的概念是很重要的。如果此变量被设置成
false 状态,那么等待的线程将要被阻断,直到此变量进入
true 状态;任何等待 true 的条件变量的等待线程会被自动释放。
(在这种情况下,wait() 调用不会发生阻断。)。通过如下扩展
notify() 的语法,可以支持这个功能:


notify();
释放所有等待的线程,而不改变其下面的条件变量的状态。



notify(true);
把条件变量的状态设置为 true 并释放任何等待的进程。其后对于
wait() 的调用不会发生阻断。



notify(false);
把条件变量的状态设置为 false (其后对于 wait() 的调用会发生阻断)。





嵌套监控锁定问题非常麻烦,我并没有简单的解决办法。嵌套监控锁定是一种死锁形式,当某个锁的占有线程在挂起其自身之前不释放锁时,会发生这种嵌套监控封锁。
下面是此问题的一个例子(还是假设的),但是实际的例子是非常多的:


class Stack
{
LinkedList list = new LinkedList();

public synchronized void push(Object x)
{ synchronized(list)
{ list.addLast( x );
notify();
}
}

public synchronized Object pop()
{ synchronized(list)
{ if( list.size() <= 0 )
wait();
return list.removeLast();
}
}

}




此例中,在 get() 和 put() 操作中涉及两个锁:一个在 Stack 对象上,另一个在 LinkedList
对象上。下面我们考虑当一个线程试图调用一个空栈的 pop() 操作时的情况。此线程获得这两个锁,然后调用 wait() 释放 Stack 对象上
的锁,但是没有释放在 list 上的锁。如果此时第二个线程试图向堆栈中压入一个对象,它会在 synchronized(list) 语句上永远挂起,
而且永远不会被允许压入一个对象。由于第一个线程等待的是一个非空栈,这样就会发生死锁。这就是说,第一个线程永远无法从 wait() 返回,因为由于它占据着锁,而导致第二个线程永远无法运行到 notify() 语句。




在这个例子中,有很多明显的办法来解决问题:例如,对任何的方法都使用同步。但是在真实世界中,解决方法通常不是这么简单。


一个可行的方法是,在 wait() 中按照反顺序释放当前线程获取的所有锁,然后当等待条件满足后,重新按原始获取顺序取得它们。
但是,我能想象出利用这种方式的代码对于人们来说简直无法理解,所以我认为它不是一个真正可行的方法。如果您有好的方法,请给我发 e-mail。


我也希望能等到下述复杂条件被实现的一天。例如:

(a && (b || c)).wait();
其中 a、b 和 c 是任意对象。

原文转自:http://www.ltesting.net