在多线程代码中,使用驱动其它线程所负责的动作的单个主线程是常见的。这个主线程发送消息,通常是通过把它们放到一个队列中,然后其它线程处理这些消息。但是如果主线程抛出一个异常,那么剩余的线程会继续运行,等待更多输入到该队列,导致程序冻结。在诊断 Java 代码的这一部分中,专职 Java 开发者兼兼职捉虫者 Eric Allen 讨论检测、修复和避免这一错误模式。
用多线程编写代码对程序员大有好处。多线程能使编程(和程序)进行得快得多,而且代码能有效得多地使用资源。然而,跟生活中的很多事情一样,多线程也存在缺点。因为多线程代码天生是非确定性的,出现错误的可能性大得多。而且,确实发生的的错误很难重现,因此也更难解决。
孤线程模式
Java 编程语言为多线程代码提供了丰富的支持,包括一项特别有用的功能:能够在一个线程中抛出一个异常而不影响其它线程。但这项功能会导致很多难以跟踪的错误。
从某个线程的崩溃中恢复过来是有意义,在此种情况下,这种能力能增加程序的健壮性级别。然而,它也使我们难以判断这些线程之一在什么时候抛出了一个异常。因为剩余的线程将继续运行,所以程序会表现出无响应或冻结程序的征兆。对线程之间频繁通信的程序而言尤其如此。
考虑清单 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"); } 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"); } 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 方法破坏了语言的安全性模型。
总结
这里是本周错误模式的总结:
模式:孤线程
症状:一个锁定多线程程序,它具有或不具有将堆栈跟踪打印到标准错误。
致因:多个程序线程一直等待来自某个线程的输入,而该线程在抛出一个未被捕捉的异常后就退出了。
治疗和预防措施:把异常处理代码放到主线程中以在崩溃来临之际通知依赖线程。
发光并非太阳的专利,你也可以发光