内容:
孤线程模式
症状
治疗和预防措施
总结
参考资料
在多线程代码中,使用驱动其它线程所负责的动作的单个主线程是常见的。这个主线程发送消息,通常是通过把它们放到一个队列中,然后其它线程处理这些消息。但是假如主线程抛出一个异常,那么剩余的线程会继续运行,等待更多输入到该队列,导致程序冻结。在诊断 Java 代码的这一部分中,专职 Java 开发者兼兼职捉虫者 Eric Allen 讨论检测、修复和避免这一错误模式。请在讨论论坛与作者和其他读者共享您关于本文的心得。
用多线程编写代码对程序员大有好处。多线程能使编程(和程序)进行得快得多,而且代码能有效得多地使用资源。然而,跟生活中的很多事情一样,多线程也存在缺点。因为多线程代码天生是非确定性的,出现错误的可能性大得多。而且,确实发生的的错误很难重现,因此也更难解决。
孤线程模式
Java 编程语言为多线程代码提供了丰富的支持,包括一项非凡有用的功能:能够在一个线程中抛出一个异常而不影响其它线程。但这项功能会导致很多难以跟踪的错误。
快速跟踪代码
清单 1.
一个线程之间频繁通信的示例程序
清单 2.
演示如何捕捉异常并在退出之前通知问题的依靠线程。
从某个线程的崩溃中恢复过来是有意义,在此种情况下,这种能力能增加程序的健壮性级别。然而,它也使我们难以判定这些线程之一在什么时候抛出了一个异常。因为剩余的线程将继续运行,所以程序会表现出无响应或冻结程序的征兆。对线程之间频繁通信的程序而言尤其如此。
考虑清单 1 所示的示例,其中的一对线程通过生产者-消费者模型进行通信。
清单 1. 一个简单的、多线程的消费者-生产者程序
public class Server extends Thread {
Client client;
int counter;
public Server(Client _client) {
this.client = _client;
this.counter = 0;
}
public void run() {
while (counter < 10) {
this.client.queue.addElement(new Integer(counter));
counter++;
}
throw new RuntimeException("counter >= 10");
}
public static void main(String[] args) {
Client c = new Client();
Server s = new Server(c);
c.start();
s.start();
}
}
class Client extends Thread {
Vector queue;
public Client() {
this.queue = new Vector();
}
public void run() {
while (true) {
if (! (queue.size() == 0)) {
processNextElement();
}
}
}
private void processNextElement() {
Object next = queue.elementAt(0);
queue.removeElementAt(0);
System.out.println(next);
}
}
在诸如这样的案例中,第二个线程接收用于计算的任何数据完全依靠于第一个线程。因此,不可避免地,假如第一个线程崩溃(而在这个样本中,肯定是这样的),那么第二个线程将等待永远不会到来的更多输入。现在您知道我为什么把这种错误叫做孤线程模式。
症状
这种错误模式最常见的症状是我在前面提到的 ? 即,程序好象冻结了。
其它症状可能包括打印到程序标准错误和标准输出的堆栈跟踪实际停止了。
治疗和预防措施
一旦诊断出这种错误模式,查找并修复在崩溃线程中的潜在的错误是显然的治疗之道。但是预防却困难得多。
不用说,假如您使用单线程设计就能侥幸成功,那么将可以免除很多头痛的事情。然而,当程序的性能要求是必须考虑的问题时,就要首先考虑使用多线程设计。
诊断这种崩溃的一个辅助手段是捕捉由各种线程抛出的异常并在退出之前通知该问题的依靠线程。这正是我在清单 2 中所做的。
清单 2. 把错误通知给客户机线程的示例
import java.util.Vector;
public class Server2 extends Thread {
Client2 client;
int counter;
public Server2(Client2 _client) {
this.client = _client;
this.counter = 0;
}
public void run() {
try {
while (counter < 10) {
this.client.queue.addElement(new Integer(counter));
counter++;
}
throw new RuntimeException("counter >= 10");
}
catch (Exception e) {
this.client.interruptFlag = true;
throw new RuntimeException(e.toString());
}
}
public static void main(String[] args) {
Client2 c = new Client2();
Server2 s = new Server2(c);
c.start();
s.start();
}
}
class Client2 extends Thread {
Vector queue;
boolean interruptFlag;
public Client2() {
this.queue = new Vector();
this.interruptFlag = false;
}
public void run() {
while (! interruptFlag) {
if (! (queue.size() == 0)) {
processNextElement();
}
}
// Processes whatever elements remain on the queue before exiting.
while (! (queue.size() == 0)) {
processNextElement();
}
System.out.flush();
}
private void processNextElement() {
Object next = queue.elementAt(0);
queue.removeElementAt(0);
System.out.println(next);
}
}
处理被抛出的异常的其它选项可能是调用 System.exit。这个选项在程序的主线程发生崩溃而其它线程不治理任何临界资源的时候是有意义的。然而在其它情况下,这可能是危险的。例如,考虑这样一个示例,其它线程中的一个正在治理一个打开的文件。假如这是实际的情况,那么只是退出程序会导致资源泄漏。
即使在上面的简单示例中,在 server 线程中调用 System.exit 也会导致 client 未处理其队列上的任何剩余元素就退出。
事实上,就是这样的问题促使 Sun 不建议线程的 stop 方法。由于强行停止一个线程会使资源陷入非一致状态,所以 stop 方法破坏了语言的安全性模型。
想了解 Sun 的更多不建议理由,请参阅参考资料。
总结
这里是本周错误模式的总结:
模式:孤线程
症状:一个锁定多线程程序,它具有或不具有将堆栈跟踪打印到标准错误。
致因:多个程序线程一直等待来自某个线程的输入,而该线程在抛出一个未被捕捉的异常后就退出了。
治疗和预防措施:把异常处理代码放到主线程中以在崩溃来临之际通知依靠线程。
参考资料
参加本文的讨论论坛。
阅读关于为什么不建议 Thread.stop 的 Sun 的解释。
Neel V. Kumar 在他的文章“Java 程序中的多线程”(developerWorks,2000 年 3 月)中提供调试多线程 Java 的途径。
Peter Haggar 的“优化 Java 编程中的并发”(IBM PartnerWorld for Developers)是一份优秀的白皮书,它讨论通过执行多线程来并发存取数据会导致的常见问题。
想获得编写多线程 Java 程序的介绍,请参阅 Alex Roetter 的文章“编写多线程 Java 应用程序”(developerWorks,2001 年 2 月)。
想为您的 Java 应用程序中的多线程问题获得技术帮助,请访问多线程 Java 编程讨论论坛。
Brian Goetz 在他共三部分的系列轻松使用线程中处理困难的线程问题。
JUnit 主页提供讨论程序测试方法的很多有趣文章的链接,并提供 JUnit 的最新版本。
利用 Java 调试教程(developerWorks,2001 年 2 月),获取通用调试技术的帮助。
阅读 Eric 所有诊断 Java 代码的文章,其中多数着重讨论错误模式。
请在 developerWorks Java 技术专区查找更多的 Java 参考资料。