`

Java 理论与实践:嗨,我的线程到哪里去了?

 
阅读更多
如果您不小心,线程可能会在没有(堆栈)跟踪的情况下从服务器应用程序中消失。在本文中,线程问题专家 Brian Goetz 提供了用于预防和检测线程“擅离职守”的技术。

当单线程应用程序中的主线程抛出一个未捕获的异常时,因为控制台中会打印堆栈跟踪(也因为程序停止),所以您很可能注意到。但在多线程应用程序中,尤其是在作为服务器运行并且不与控制台相连的应用程序中,线程死亡可能成为不太引人注目的事件,这会导致局部系统失败,从而产生混乱的应用程序行为。

Java theory and practice 十月份的专栏文章中,我们研究了线程池,并研究了编写得不正确的线程池会如何“泄漏”线程,直到最终丢失所有线程。大多数线程池实现通过捕获抛出的异常或重新启动死亡的线程来防止这一点,但线程泄漏的问题并不仅限于线程池 — 使用线程来为工作队列提供服务的服务器应用程序也可能具有这种问题。当服务器应用程序丢失了一个工作线程(worker thread)时,在较长时间内应用程序仍可能显得一切正常,这使得该问题的真实原因难以确定。

许多应用程序用线程来提供后台服务 — 处理来自事件队列的任务、从套接字读取命令或执行 UI 线程以外的长期任务。当由于抛出未捕获的 RuntimeExceptionError,或者只是停下来,等待阻塞的 I/O 操作(原本未预计到阻塞),从而引起这些线程之一死亡时,会发生什么呢?

有时,譬如当线程执行由用户启动的长期任务(如拼写检查)时,用户会注意到任务没有进展,他们可能会异常终止操作或程序。但其它时间,后台线程执行“清理维护”任务 ,它们可能消失很长时间而不被察觉。

示例服务器应用程序
考虑这样一个假设的中间件服务器应用程序,它聚合来自各种输入源的消息,然后将它们提交到外部服务器应用程序,从外部应用程序接收响应并将响应路由回适当的输入源。对于每个输入源,都有一个以其自己的方式接受其输入消息的插件(通过扫描文件目录、等待套接字连接、轮询数据库表等)。插件可以由第三方编写,即使它们是在服务器 JVM 上运行的。这个应用程序拥有(至少)两个内部工作队列 — 从插件处接收的正在等待被发送到服务器的消息(“出站消息”队列),以及从服务器接收的正在等待被传递到适当插件的响应(“入站响应”队列)。通过调用插件对象上的服务例程 incomingResponse(),消息被路由到最初发出请求的插件。

从插件接收消息后,就被排列到出站消息队列中。由一个或多个从队列读取消息的线程处理出站消息队列中的消息、记录其来源并将它提交给远程服务器应用程序(假定通过 Web 服务接口)。远程应用程序最终通过 Web 服务接口返回响应,然后我们的服务器将接收的响应排列到入站响应队列中。一个或多个响应线程从入站响应队列读取消息并将其路由到适当的插件,从而完成往返“旅程”。

在这个应用程序中,有两个消息队列,分别用于出站请求和入站响应,不同的插件内可能也有另外的队列。我们还有几种服务线程,一个从出站消息队列读取请求并将其提交给外部服务器,一个从入站响应队列读取响应并将其路由到插件,在用于向套接字或其它外部请求源提供服务的插件中可能也有一些线程。

线程失败时并不总是显而易见的
如果这些线程中的一个(如响应分派线程)消失了,将会发生什么?因为插件仍能够提交新消息,所以它们可能不会立即注意到某些方面出错了。消息仍将通过各种输入源到达,并通过我们的应用程序提交到外部服务。因为插件并不期待立即获得其响应,因此它仍没有意识到出了问题。最后,接收的响应将排满队列。如果它们存储在内存中,那么最终将耗尽内存。即使不耗尽内存,也会有人在某个时刻发现响应得不到传递 — 但这可能需要一些时间,因为系统的其它方面仍能正常发挥作用。

当主要的任务处理方面由线程池而不是单个线程来处理时,对于偶然的线程泄漏的后果有一定程度的保护,因为一个执行得很好的八线程的线程池,用七个线程完成其工作的效率可能仍可以接受。起初,可能没有任何显著的差异。但是,系统性能最终将下降,虽然这种下降的方式不易被察觉。

服务器应用程序中的线程泄漏问题在于不是总是容易从外部检测它。因为大多数线程只处理服务器的部分工作负载,或可能仅处理特定类型的后台任务,所以当程序实际上遭遇严重故障时,在用户看来它仍在正常工作。这一点,再加上引起线程泄漏的因素并不总是留下明显痕迹,就会引起令人惊讶甚或使人迷惑的应用程序行为。

RuntimeException 是导致线程死亡的首要原因
当线程抛出未捕获的异常或错误时它们可能消失;而当线程等待的 I/O 操作永远不会完成,或没人为它们等待的监视器调用 notify() 时,它们只是停止工作。意外线程死亡的最常见根源是 RuntimeException(如 NullPointerExceptionArrayIndexOutOfBoundsException 等)。在我们的示例应用程序中,在通过调用插件对象上的 incomingResponse() 将响应传递回插件时,可能抛出 RuntimeException。插件代码可能是由第三方编写的,或者可能是在编写完应用程序之后编写的,因此应用程序编写者不可能审核其正确性。如果一些插件抛出 RuntimeException 时某些响应服务线程会终止,这意味着一个出错的插件会使整个系统崩溃。遗憾的是,这种脆弱性很常见。

尽管编译器“希望”我们主动地针对已查出的异常编制代码(编译器会强制我们这样做),但大多数 Java 开发人员在很大程度上忽略了未查出的异常。在单线程应用程序中,未经处理的 RuntimeException 的结果很明显,并且对发生异常的位置有明确的堆栈跟踪,这提供了问题通知以及解决问题的有用信息。但是,在多线程应用程序中,由于未查出的异常,线程会无声无息地死亡 — 使得用户和开发人员对于发生的问题和为什么发生这些问题毫无头绪。

处理任务的线程(类似于示例应用程序中的请求和响应处理程序),基本上花费其整个生命周期穿过某个类似于 Runnable 的抽象障碍物来调用服务方法。因为我们不知道在这个抽象障碍物的另一边是什么,所以,对于服务方法,我们应该怀疑,它是不是真好到可以假设它从不抛出未查出异常的程度。如果服务例程抛出 RuntimeException,则调用线程应该捕获这个异常,并将它记录到日志,然后转到队列中的下一项或关闭线程然后再重新启动它。(后一个选项源自这样的假定:任何抛出 RuntimeException 或 Error 的代码也可能已经破坏了线程的状态。)

清单 1 中的代码是典型的从工作队列处理 Runnable 任务的线程,类似于我们的示例中的入站响应线程。它并不防备抛出任何未查出异常的插件。

清单 1. 不防备 RuntimeException 的工作线程


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 中所示:

清单 2. 防备 RuntimeException 的工作线程


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 中的 Thread 添加了未捕获的异常处理为止),uncaughtException 特性暂时使它不可或缺。清单 3 展示了一个示例,使用 ThreadGroup 来检测由于未捕获的异常引起的线程死亡。

清单 3. 使用 uncaughtException 来检测线程死亡


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 处理程序来在线程异常终止时进行检测。

分享到:
评论

相关推荐

    Java理论与实践:嗨,我的线程到哪里去了?

    本文介绍了当线程从应用程序中消失时会引起混乱,并且在很多情况下,线程消失时没有(堆栈)跟踪。像对付许多风险一样,防止线程泄漏的最佳方法是预防和检测相结合;注意有可能抛出RuntimeException的地方(如调用...

    Java理论与实践:并发在一定程度上使一切变得简单

    本文介绍了当项目中需要XML解析器、文本索引程序和搜索引擎、正则表达式编译器、XSL处理器或PDF生成器时,大多数人从不会考虑自己去编写这些实用程序。并介绍了util.concurrent包包含许多有用的类。它们是许多多线程...

    Java理论与实践:描绘线程安全性

    本文定义了线程安全性,类要成为线程安全的,首先必须在单线程环境中有正确的行为。并在被多个线程访问时,不管运行时环境执行这些线程有什么样的时序安排,它必须有如上所述的正确行为,并且在调用的代码中没有任何...

    Java 理论与实践: 正确使用 volatile 变量 线程同步

    Java语言规范中指出:为了获得佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。  这样当多个线程同时与某个对象交互时,必须要注意到要让线程...

    Java理论与实践:变还是不变?

    并说明了在Java理论与实践中,不变性的一些长处、何时使用不变类和构造不变类的一些准则。使用不变对象比使用可变对象要容易得多。它们只能处于一种状态,所以始终是一致的,它们本来就是线程安全的,可以被自由地...

    Java理论与实践:非阻塞算法简介

    本文介绍了在Java理论与实践中,几种比较简单的非阻塞算法的工作方式。在不只一个线程访问一个互斥的变量时,所有线程都必须使用同步,否则就可能会发生一些非常糟糕的事情。Java语言中主要的同步手段就是...

    Java理论与实践:修复Java内存模型1

    本文介绍了Java平台把线程和多处理技术集成到了语言中,这种集成程度比以前的大多数编程语言都要强很多。关于同步和线程安全的许多底层混淆是Java内存模型的一些难以直觉到的细微差别。本文还介绍了JMM有一些严重的...

    Java理论与实践:修复Java内存模型2

    本文介绍了Java平台从一开始就包括了对线程的支持,包括一个计划为正确同步的程序提供“一次编写,到处运行”保证的、跨平台的内存模型,但是原来的内存模型有一些漏洞。虽然许多Java平台提供了比JMM所要求的更强的...

    Java理论与实践:线程池与工作队列

    另一个常见的线程模型是为某一类型的任务分配一个后台线程与任务队列。AWT和Swing就使用这个模型,在这个模型中有一个GUI事件线程,导致用户界面发生变化的所有工作都必须在该线程中执行。然而,由于只有一个AWT线程...

    Java理论与实践:流行的原子

    本文介绍了要使用多处理器系统的功能,通常需要使用多线程构造应用程序。但是正如任何编写并发应用程序的人可以告诉你的那样,要获得好的硬件利用率,只是简单地在多个线程中分割工作是不够的,还必须确保线程确实大...

    Java理论与实践:并发集合类

    本文介绍了在Java类库中出现的第一个关联的集合类是Hashtable,它是JDK 1.0的一部分。Hashtable提供了一种易于使用的、线程安全的、关联的map功能,这当然也是方便的。然而,线程安全性是凭代价换来的―― Hashtable...

    Java理论与实践:Mustang中的同步优化

    每当易变的变量在线程间共享时,都必须使用同步来确保一个线程所做的更新,能够及时地被其他线程看到。同步的主要方式就是使用synchronized块,它既提供了互斥又提供了可见性保证。当两个线程都想访问共享的易变变量...

    Java理论与实践:消除bug

    本文介绍了编写线程安全的类很难,而分析现有类的线程安全性更难,增强类使其仍然保持线程安全也很难。随着FindBugs的引入,在自动代码检测和审核工具方面已经取得重大进步。FindBugs利用字节码分析和很多内置的bug...

    Java理论与实践:JVM 1.4.1中的垃圾收集

    并简单概述了老对象和年轻对象、分代收集、小的收集、代间引用、跟踪代间引用、卡片标记、JDK 1.4.1 默认收集器、并行收集器和并发收集器、微调垃圾收集器等理论或技术。得出:随着JVM的发展,默认垃圾收集器变得...

    Java 并发学习笔记:进程和线程,并发理论,并发关键字,Lock 体系,原子操作类,发容器 & 并发工具,线程池,并发实践

    Java 并发学习笔记: 进程和线程, 并发理论, 并发关键字, Lock 体系, 原子操作类, 发容器 & 并发工具, 线程池, 并发实践 Java是一种面向对象的编程语言,由Sun Microsystems于1995年推出。它是一种跨平台的...

    Java理论与实践:JDK 5.0中更灵活、更具可伸缩性的锁定机制

    本文介绍了多线程和并发性并不是什么新内容,但是Java语言设计中的创新之一就是,它是第一个直接把跨平台线程模型和正规的内存模型集成到语言中的主流语言。核心类库包含一个Thread类,可以用它来构建、启动和操纵...

    Java理论与实践:做个好的(事件)侦听器

    本文介绍了AWT 和Swing组件使用观察者模式消除了GUI事件生成与它们在指定应用程序中的语义之间的耦合...侦听器涉及的任何对象,都应当是线程安全的,或者是受线程约束的对象,侦听器应当确定自己正在正确的线程中执行。

    concurrent 多线程 教材

    13 Java 理论与实践: 描绘线程安全性 (2) 14 编写高效的线程安全类 (2) 15 轻松使用线程 同步不是敌人.mht 16 轻松使用线程 减少争用.mht 17 轻松使用线程 不共享有时是最好的.mht 18 适用于 Java 程序员的 CSP...

    Java性能优化实战视频全集

    01 理论分析:性能优化,有哪些衡量指标?需要注意什么?.mp4 02 理论分析:性能优化有章可循,谈谈常用的切入点.mp4 03 深入剖析:哪些资源,容易成为瓶颈?.mp4 04工具实践:如何获取代码性能数据?.mp4 05 工具...

    Java理论与实践:修复Java内存模型,第1部分

    在这一期的Java理论与实践中,BrianGoetz展示了如何加强volatile和final的语义,以修复JMM。这些更改有些已经集成在JDK1.4中;而另一些将会包含在JDK1.5中。您可以在本文对应的论坛里与作者及其他读者分享您对本文的...

Global site tag (gtag.js) - Google Analytics