Java:如何修复线程挂起?

4
请注意:我将此标记为JClouds,因为如果您阅读整个问题和随后的评论,我认为这要么是JClouds的错误,要么是对该库的误用。
我有一个可执行的JAR文件,运行一段时间后工作正常,完成工作时不会抛出任何错误/异常,但当它应该退出时却永远挂起。我使用VisualVM对其进行了分析(关注正在运行的线程),并在应用程序挂起的位置(在main()方法的末尾)添加了日志语句。以下是我的主要方法的最后部分:
Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
for(Thread t : threadSet) {
    String daemon = (t.isDaemon()? "Yes" : "No");
    System.out.println("The ${t.getName()} thread is currently running; is it a daemon? ${daemon}.");
}

当我的JAR执行此代码时,我会看到以下输出:
The com.google.inject.internal.util.Finalizer thread is currently running; is it a daemon? Yes.
The Signal Dispatcher thread is currently running; is it a daemon? Yes.
The RMI Scheduler(0) thread is currently running; is it a daemon? Yes.
The Attach Listener thread is currently running; is it a daemon? Yes.
The user thread 3 thread is currently running; is it a daemon? No.
The Finalizer thread is currently running; is it a daemon? Yes.
The RMI TCP Accept-0 thread is currently running; is it a daemon? Yes.
The main thread is currently running; is it a daemon? No.
The RMI TCP Connection(1)-10.10.99.8 thread is currently running; is it a daemon? Yes.
The Reference Handler thread is currently running; is it a daemon? Yes.
The JMX server connection timeout 24 thread is currently running; is it a daemon? Yes.

我不认为我需要担心守护进程(如果我错了请纠正),因此将其过滤为非守护进程:

The user thread 3 thread is currently running; is it a daemon? No.
The main thread is currently running; is it a daemon? No.

显然,主线程仍在运行,因为有些东西阻止它退出。嗯,用户线程3看起来很有趣。VisualVM告诉我们什么?
这是应用程序挂起的点的线程视图(上面的控制台输出打印时正在发生什么)。嗯,用户线程3看起来更可疑了!
所以在杀死应用程序之前,我获取了一个线程转储。这是用户线程3的堆栈跟踪:
"user thread 3" prio=6 tid=0x000000000dfd4000 nid=0x2360 waiting on condition [0x00000000114ff000]
    java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x0000000782cba410> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
        at java.lang.Thread.run(Thread.java:744)

    Locked ownable synchronizers:
        - None

我以前从未分析过这类内容,所以对我来说是无意义的(但对受过训练的人来说可能不是!)。

在结束应用程序后,VisualVM的时间轴停止每秒钟滴答/递增,我可以横向向后滚动时间线到用户线程3被创建并开始作为一个令人讨厌的线程生活的地方:

enter image description here

然而,我无法弄清楚如何确定在哪个代码中创建了用户线程3。所以我问:

  • 如何确定是谁创建了用户线程3,在哪里(特别是因为我怀疑它是由第三方OSS库创建的线程)?
  • 如何诊断、诊断和修复这个线程挂起问题?

更新:

以下是我的代码,大约在创建用户线程3时触发:

ExecutorService myExecutor = Executors.newCachedThreadPool();
for(Node node : nodes) {
    BootstrapAndKickTask bootAndKickTask = new BootstrapAndKickTask(node, ctx);
    myExecutor.execute(bootAndKickTask);
}

myExecutor.shutdown();
if(!myExecutor.awaitTermination(15, TimeUnit.MINUTES)) {
    TimeoutException toExc = new TimeoutException("Hung after the 15 minute timeout was reached.");
    log.error(toExc);

    throw toExc;
}

这里还有我的GitHub Gist,其中包含完整的线程转储。


1
这看起来像是一个未正确关闭的执行器线程池。您可能需要在某些组件上调用shutdownterminateclose方法。 - Thilo
1
即使是守护线程也会参与死锁。请在此提供所有堆栈跟踪。 - Basilevs
1
@Basilevs 可能会发生这种情况。如果 ExecutorService 仍然可以访问,比如通过全局变量,那么 finalize() 方法将永远不会被调用,也不会关闭 ExecutorService。默认情况下,为 ExecutorService 创建的线程是非守护线程。 - Smith_61
1
@Zac http://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#stop%28%29。虽然不建议使用它。根据您发布的内容,您正在关闭它。这很奇怪。我猜这是另一个池被创建了。 - Smith_61
1
@Zac 如果是的话,我认为这就是池被创建的地方。在文件中搜索“用户线程%d”。https://github.com/jclouds/jclouds/blob/e711275fb132c8c2f0155400de01275653ad70e7/core/src/main/java/org/jclouds/concurrent/config/ExecutorServiceModule.java - Smith_61
显示剩余19条评论
3个回答

5
似乎发生了以下情况,但如果没有代码无法确认:您忘记在ExecutorService上调用shutdown()/shutdownNow()。您似乎将一个ThreadPoolExecutor对象全局可访问并保持运行,当主线程退出时仍然存在。由于它仍然是全局可访问的,ExecutorService将永远不会调用其finalize方法并且永远不会关闭自身。默认情况下,为ExecutorService创建的线程被创建为非守护进程,并且在不再需要时将继续运行。

您应该提供代码供我们查看,或者在使用ThreadPoolExecutor的位置浏览您的代码,并在使用完毕后正确关闭它。

根据文档:

如果程序中不再引用某个线程池并且没有剩余的线程,它将自动关闭。如果您想确保即使用户忘记调用shutdown()也能回收未引用的线程池,则必须安排未使用的线程最终死亡,通过设置适当的存活时间,使用零核心线程的下限和/或设置allowCoreThreadTimeOut(boolean)。

这意味着即使您的程序不再引用ThreadPoolExecutor,只要池中仍有至少一个线程处于活动状态,它将永远不会被回收。您可以查看文档以了解解决此问题的方法。


文档中说:“程序中不再引用且没有剩余线程的池将自动关闭。如果您希望确保即使用户忘记调用shutdown(),未引用的池也能被回收,则必须通过设置适当的保持活动时间、使用零个核心线程的下限和/或设置allowCoreThreadTimeOut(boolean)来安排未使用的线程最终死亡。” 这意味着即使它是不可访问的并且最终被终结,线程仍然不会按默认情况下被杀死。 - Michael Anderson
1
Smith是正确的。核心池中未使用的线程除非调用了shutdown,否则永远不会被销毁。该线程在调度程序队列上被锁定,并且不会消耗任何CPU。它根本没有“挂起”。它正在等待下一个任务。 - bond
@MichaelAnderson 没错。我的回答应该更清晰,只要线程池中存在一个正在运行的线程,它就永远不会被终止。因为该线程包含对线程池的引用。 - Smith_61

3
希望您能贴出您所使用的全部代码。Apache jclouds使用多个执行器来执行某些任务,您需要关闭它们。
请确保在从jclouds ContextBuilder获取的上下文或api上调用close()方法。

感谢@Ignasi Barrera (+1) - 我正在测试,稍后会回复您。我没有关闭API或ChefContext,希望这样可以解决问题! - DirtyMikeAndTheBoys
你只需要关闭直接从ContextBuilder获取的内容。在这种情况下,关闭ChefContext就足够了。 - Ignasi Barrera

1

有两个错误:

  1. 您未能对分配的资源(线程池)进行异常安全释放
  2. 您捕获了您不知道如何处理的错误。

以下是潜在的解决方法。(我不确定是否应该在 finally 块中包括等待线程完成)

ExecutorService myExecutor = Executors.newCachedThreadPool();
try {
    for(Node node : nodes) {
        BootstrapAndKickTask bootAndKickTask = new BootstrapAndKickTask(node, ctx);
        myExecutor.execute(bootAndKickTask);
    }
} finally {    
    myExecutor.shutdown();
    if(!myExecutor.awaitTermination(15, TimeUnit.MINUTES)) {
        TimeoutException toExc = new TimeoutException("Hung after the 15 minute timeout was reached.");
        log.error(toExc);
        throw toExc;
    }
}

感谢@Basilevs (+1) - 虽然这不是根本问题,但我确实采纳了您的建议,使我的代码更加安全。 - DirtyMikeAndTheBoys

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接