诊断 Java 代码: 诊断 Java 代码:孤线程(Orphaned Thread)错误模式
用多线程编写代码对程序员大有好处。多线程能使编程(和程序)进行得快得多,而且代码能有效得多地使用资源。然而,跟生活中的很多事情一样,多线程也存在缺点。因为多线程代码天生是非确定性的,出现错误的可能性大得多。而且,确实发生的的错误很难重现,因此也更难解决。
Java 编程语言为多线程代码提供了丰富的支持,包括一项特别有用的功能:能够在一个线程中抛出一个异常而不影响其它线程。但这项功能会导致很多难以跟踪的错误。
考虑清单 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 中所做的。
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 的更多不建议理由,请参阅 参考资料。
这里是本周错误模式的总结:
- 模式:孤线程
- 症状:一个锁定多线程程序,它具有或不具有将堆栈跟踪打印到标准错误。
- 致因:多个程序线程一直等待来自某个线程的输入,而该线程在抛出一个未被捕捉的异常后就退出了。
- 治疗和预防措施:把异常处理代码放到主线程中以在崩溃来临之际通知依赖线程。
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
- 参加本文的 讨论论坛。
- 阅读关于为什么不建议
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 月)。
- Brian Goetz 在他共三部分的系列 轻松使用线程 中处理困难的线程问题。
- JUnit 主页提供讨论程序测试方法的很多有趣文章的链接,并提供 JUnit 的最新版本。
- 利用 Java 调试教程(developerWorks,2001 年 2 月),获取通用调试技术的帮助。
- 阅读 Eric 所有 诊断 Java 代码的文章,其中多数着重讨论错误模式。
- 请在 developerWorks Java 技术专区查找更多的 Java 参考资料。
Eric Allen 从 Cornell 大学获得计算机科学及数学的学士学位,并且是 Rice 大学 Java 编程语言小组的博士候选人。在回 Rice 专心攻读学位前,Eric 是 Cycorp,Inc. 的 Java 开发者带头人。他还在 JavaWorld上主持 Java 初学者讨论论坛。他的研究包括在源程序和字节码级别上 Java 语言的语义模型和静态分析工具开发。Eric 还帮助开发 Rice 的 NextGen 编程语言编译器,NextGen 是一个支持泛运行时类型的 Java 扩展。可通过 [email protected]与 Eric 联系。