当单线程应用程序中的主线程引发未捕获的异常时,您可能会注意到,因为堆栈跟踪记录打印在控制台上(并且程序停止了)。 但是在多线程应用程序中,尤其是在作为服务器运行且未连接到控制台的应用程序中,线程死亡可能是不太明显的事件,从而导致部分系统故障,从而可能导致混乱的应用程序行为。
在7月的Java理论与实践系列中 ,我们研究了线程池,并研究了编写不正确的线程池如何“泄漏”线程,直到最终所有线程消失。 大多数线程池实现都可以通过捕获抛出的异常或重新启动死掉的线程来防止这种情况的发生,但是线程泄漏的问题不仅限于线程池-使用线程为工作队列提供服务的服务器应用程序也可能存在此问题。 当服务器应用程序丢失工作线程时,该应用程序可能会在相当长的一段时间内正常运行,这使得难以确定问题的真正原因。
许多应用程序使用线程来提供后台服务-从事件队列处理任务,从套接字读取命令或在UI线程外部执行长时间运行的任务。 当这些线程之一由于抛出未捕获的RuntimeException
或Error
而死,或者只是停滞下来,等待原本不应阻塞的阻塞的I / O操作时,会发生什么情况?
有时,用户会注意到没有进展,例如当线程正在执行长时间运行的用户启动的任务(例如拼写检查)时,可能会中止操作或程序。 但是有时,后台线程会执行内部管理任务,而且很长一段时间都不会注意到它们的消失。
服务器应用程序示例
考虑这个假设的中间件服务器应用程序,该应用程序聚集来自各种输入源的消息,然后将其提交给外部服务器应用程序,从外部应用程序接收响应,并将响应路由回适当的输入源。 对于每个输入源,都有一个插件以其自己的方式接收其输入消息-通过扫描文件目录,等待套接字连接,轮询数据库表等等。 插件可以由第三方编写,即使它们在服务器JVM中运行。 该应用程序具有(至少)两个内部工作队列-从插件接收的消息等待发送到服务器(“传出消息”队列),以及从服务器接收回的响应等待发送到适当的服务器插件(“传入响应”队列)。 通过在插件对象上调用服务例程incomingResponse()
,将消息路由回原始插件。
从插件收到消息后,该消息将排队到传出消息队列中。 来自传出消息队列的消息由一个或多个线程处理,这些线程从队列中读取消息,记下消息的来源,然后将其提交给远程服务器应用程序(例如,通过Web服务接口)。 远程应用程序最终通过Web服务接口进行响应,并且我们的服务器将接收到的响应排队到传入响应队列中。 一个或多个响应线程从传入的响应队列中读取消息,并将其路由到适当的插件,从而完成往返。
在此应用程序中,我们有两个消息队列-用于传出请求和传入响应-以及各种插件中的其他队列。 我们也有几个服务线程-一个从请求消息队列中读取请求并将其提交给外部服务器,另一个从请求队列中读取响应并将它们路由到插件,还有一个可能是插件中的线程-ins,用于维修套接字或其他外部请求源。
线程失败时并不总是很明显
如果其中一个线程(例如响应调度线程)消失了,会发生什么? 由于这些插件仍然可以提交新消息,因此它们可能不会立即注意到出现问题。 消息仍将通过各种输入源到达,并将通过我们的应用程序提交给外部服务。 由于该插件不会立即收到响应,因此它尚不知道存在问题。 最终,收到的响应将排队。 如果它们存储在内存中,我们最终可能会用完内存。 即使没有,有时也会注意到有人未交付响应-但是可能要花一些时间,因为系统的其他方面仍然可以正常运行。
当使用线程池而不是单线程来处理主要任务处理方面时,就可以一定程度地保证偶尔出现线程泄漏的后果,因为一个具有八个线程的良好性能的线程池可能仍会在可接受的情况下完成其工作。七。 首先,可能没有任何明显的差异。 最终,尽管性能可能会以微妙的方式下降,但最终性能会下降。
服务器应用程序中线程泄漏的问题在于,从外部进行检测并不总是很容易。 因为大多数线程仅处理服务器工作量的一部分,或者可能仅处理特定类型的后台任务,所以该程序在实际上遭受严重故障的情况下,对用户来说似乎可以正常运行。 再加上导致线程泄漏的因素并不总是留下证据的事实,可能导致令人惊讶甚至神秘的应用程序行为。
RuntimeException是线程死亡的主要原因
当线程抛出未捕获的异常或错误时,线程可能会消失,或者在等待永远不会完成的I / O操作或没有人会调用notify()
的监视器时,它们可能只是停止工作。 意外线程死亡的最常见原因是抛出RuntimeException
(例如NullPointerException
, ArrayIndexOutOfBoundsException
等)。 在我们的示例应用程序中,可能会发生RuntimeException
一个区域是通过调用插件对象上的incomingResponse()
响应将响应返回给插件。 插件代码可能是由第三方编写的,或者可能是在编写应用程序之后编写的,因此,应用程序编写者无法对其进行审核是否正确。 如果响应服务线程在某个插件抛出RuntimeException
时终止,则意味着一个有故障的插件可以关闭整个系统。 不幸的是,此漏洞非常普遍。
尽管我们期望针对检查后的异常积极地进行编码-编译器迫使我们这样做-未经检查的异常在大多数Java开发人员中都被忽略了。 在单线程应用程序中,未处理的RuntimeException
的结果是显而易见的,并且对发生的位置有清晰的堆栈跟踪,这既提供了问题的通知,也提供了修复问题的有用信息。 但是,在多线程应用程序中,线程会由于未经检查的异常而静默地死掉,从而导致用户和开发人员为所发生的事情和原因scratch之以鼻。
任务处理线程(例如我们示例应用程序中的请求和响应处理程序线程)基本上会通过诸如Runnable
类的抽象屏障来终生调用服务方法。 由于我们不知道抽象障碍的另一面是什么,因此我们应该怀疑服务方法的行为如此之好,以至于我们可以假设它永远不会抛出未经检查的异常。 如果服务例程抛出RuntimeException
,则调用线程应捕获该异常并进行记录,然后移至队列中的下一项,或者断开线程并重新启动它。 (后一种选择基于这样一个假设,即抛出RuntimeException
或Error的人也可能破坏了线程状态。)
清单1中的代码是一个典型的线程,该线程处理工作队列中的Runnable
任务,例如本例中的传入响应线程。 它不能防止插件引发任何未经检查的异常。
private class TrustingPoolWorker extends Thread {
public void run() {
IncomingResponse ir;
while (true) {
ir = (IncomingResponse) queue.getNext();
PlugIn plugIn = findPlugIn(ir.getResponseId());
if (plugIn != null)
plugIn.handleMessage(ir.getResponse());
else
log("Unknown plug-in for response " + ir.getResponseId());
}
}
}
我们不必添加大量代码来使此工作线程对插件代码中的故障更加健壮。 通过简单地捕获RuntimeException
然后采取纠正措施,我们可以确保自己免受单个编写不当的插件的破坏,从而破坏了整个服务器。 适当的纠正措施可能是记录错误,然后继续移至下一条消息,终止当前线程并重新启动它(这是TimerTask
类的工作),或卸载导致问题的插件,如图所示。清单2:
private class SaferPoolWorker extends Thread {
public void run() {
IncomingResponse ir;
while (true) {
ir = (IncomingResponse) queue.getNext();
PlugIn plugIn = findPlugIn(ir.getResponseId());
if (plugIn != null) {
try {
plugIn.handleMessage(ir.getResponse());
}
catch (RuntimeException e) {
// Take some sort of action;
// - log the exception and move on
// - log the exception and restart the worker thread
// - log the exception and unload the offending plug-in
}
}
else
log("Unknown plug-in for response " + ir.getResponseId());
}
}
}
使用ThreadGroup提供的未捕获的异常处理程序
除了将外来代码更可能引发RuntimeException
,明智的是使用ThreadGroup
类的uncaughtException
工具。 ThreadGroup
并不是很有用,但是暂时(直到在JDK 1.5中将uncaughtException
捕获的异常处理添加到Thread
中为止), uncaughtException
功能使其成为必不可少的。 清单3显示了一个使用ThreadGroup
来检测线程何时由于未捕获的异常而死亡的示例:
public class ThreadGroupExample {
public static class MyThreadGroup extends ThreadGroup {
public MyThreadGroup(String s) {
super(s);
}
public void uncaughtException(Thread thread, Throwable throwable) {
System.out.println("Thread " + thread.getName()
+ " died, exception was: ");
throwable.printStackTrace();
}
}
public static ThreadGroup workerThreads =
new MyThreadGroup("Worker Threads");
public static class WorkerThread extends Thread {
public WorkerThread(String s) {
super(workerThreads, s);
}
public void run() {
throw new RuntimeException();
}
}
public static void main(String[] args) {
Thread t = new WorkerThread("Worker Thread");
t.start();
}
}
如果线程组中的线程由于抛出未捕获的异常而死亡,则将调用线程组的uncaughtException()
方法,该方法然后可以将条目写入日志,重新启动线程,重新启动系统或采取任何纠正或诊断措施认为有必要。 至少,如果所有线程在线程死亡时都写了一条日志消息,那么您将记录出哪里出错了以及在哪里,而不仅仅是想知道您的请求处理线程去了哪里。
摘要
当线程从应用程序中消失时,这可能会造成混乱,并且很多时候,线程消失而没有(堆栈)跟踪。 像许多风险一样,防止线程泄漏的最佳方法是结合预防和检测。 请注意可能会抛出RuntimeException
地方,例如在调用外部代码时,并使用ThreadGroup
提供的uncaughtException
处理函数来检测线程何时意外终止。
翻译自: https://www.ibm.com/developerworks/java/library/j-jtp0924/index.html