【问题标题】:What happens to the ThreadPoolExecutor when thread dies in Java当 Java 中的线程死亡时,ThreadPoolExecutor 会发生什么
【发布时间】:2019-07-16 11:14:52
【问题描述】:

我创建了一个线程,该线程又创建了一个ThreadPoolExecutor 并向其提交了一些长时间运行的任务。在某些时候,原始线程由于未处理的异常/错误而死亡。执行程序应该发生什么(它是那个死线程的本地,没有对其的外部引用)?它应该被GCed还是不被GCed?

编辑:这个问题从一开始就提出错误,但我会留下它,因为 Gray 提供了一些关于 TPE 如何工作的详细信息。

【问题讨论】:

  • 任何失去对它的所有引用的对象都有资格进行垃圾回收。你想多了。
  • 没错,但这并不是我要问的。问题与衍生线程有关,抱歉我不够清楚。
  • 你的“编辑:”应该被删除@vlaku。我不认为这是正确的。看我的回答。
  • InThreadPoolExecutor 捕获您的Runnable 引发的所有异常。 Thread 永远不会死,直到游泳池想要它。如果你有一个固定大小的池,除非池被关闭,否则没有线程会因为你的代码而终止。你可以自己去看看ThreadPoolExeucotr中的所有代码。
  • 如果有帮助,记得采纳答案。

标签: java multithreading threadpool executorservice


【解决方案1】:

线程被称为GC roots。这意味着无法收集正在运行(或未启动)的线程。这也意味着无法收集从这些线程中引用的对象,这就是为什么您可以执行 new Thread(new MyRunnable()).start() 之类的操作,或者在没有任何引用的情况下运行线程池。

如果线程是守护进程,如果所有其他非守护线程都停止,它可以自动停止。您可以拥有带有守护线程的线程池,但最好的办法是确保正确清理事物(即异常不应终止本应最终停止并清理线程池的线程)。

【讨论】:

  • 这是因为它最初包含一些不适合@GhostCat 的侮辱性语言。查看编辑历史记录。
  • 是的,它们是 GC Roots,但您对为什么以及何时发生这种情况感到困惑。 Thread GC Roots 包含您的调用堆栈对象,这可以防止对象在传递给函数时被收集。 new Thread(new Runnable()) 内部的 new Runnable() 不能被 GC 处理,因为 Runnable 是名为 targetThread 类中的引用变量。 Thread 和调用堆栈中的对象在调用堆栈释放它们或线程终止之前无法收集。如果 Thread 未运行且未被引用,则随时可供收集。
  • 守护线程与这个问题无关。守护线程只影响 JVM 是否会终止。
【解决方案2】:

executor 应该发生什么(它是死线程的本地,没有外部引用)?是否应该被GCed?

答案比“是的,如果没有引用它就会是”更复杂。这取决于ThreadPoolExecutor 中运行的线程是否仍在运行。这又取决于创建的 TPE 类型以及提交给它的“长时间运行的任务”是否已完成。

例如,如果任务尚未完成,那么线程仍将运行。 即使他们已经完成,如果您的 TPE 的核心线程没有设置allowCoreThreadTimeOut(true),那么线程也不会停止。 JVM 从不垃圾收集正在运行的线程,因为它们被认为是GC "roots"

...根据定义,正在运行的线程不受 GC 的影响。 GC 通过扫描“根”开始其工作,这些“根”被认为始终可以访问;根包括全局变量(Java-talk 中的“静态字段”)和所有正在运行的线程的堆栈...

所以下一个问题是线程是否有对ThreadPoolExecutor 的引用返回,我相信他们有。 Worker 内部类是存储在 thread.target 中的 Runnable 并由 Thread 执行,因此它不能被 GC 处理。 Worker 不是 static,因此它隐含了对外部 ThreadPoolExecutor 实例的引用。 run() 方法实际上调用了ThreadPoolExecutor.runWorker() 方法,该方法引用了ThreadPoolExecutor 管理的所有任务队列。因此,正在运行的线程保留对 Worker 和 TPE 的引用,因此垃圾收集器无法收集 TPE。

例如,这是一个引用 TPE 的运行池线程的典型堆栈帧:

java.lang.Thread.sleep(Native Method)
com.j256.GcTester$1.run(GcTesteri.java:15)
java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
java.util.concurrent.FutureTask.run(FutureTask.java:266)
>> java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
>> java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
java.lang.Thread.run(Thread.java:748)

但是,如果线程池任务已全部完成并且它有 0 个核心线程或核心线程已超时,那么将没有与 ThreadPoolExecutor 关联的 Worker 线程。然后 TPE 被垃圾回收,因为除了循环引用之外没有对它的引用,GC 足够聪明地检测到。

这是一个演示它的小示例测试程序。如果有 1 个核心线程,则 TPE 将永远不会关闭(通过finalize()),即使在注意到/tmp/x 文件存在后工作线程退出后也是如此。即使主线程没有引用它也是如此。但是,如果有 0 个核心线程,那么在线程超时后(这里是在完成最后一个任务后 1 秒后)将收集 TPE。

public class GcTester {
    public static void main(String[] args) {
        int numCore = 1; // set to 0 to have it GC'd once /tmp/x file exists
        ExecutorService pool =
                new ThreadPoolExecutor(numCore, Integer.MAX_VALUE,
                        1, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()) {
                    protected void terminated() {
                        System.out.println(this + " terminated");
                    }
                };
        pool.submit(new Runnable() {
            public void run() {
                while (true) {
                    Thread.sleep(100); // need to handle exception here
                    if (new File("/tmp/x").exists()) {
                        System.out.println("thread exiting");
                        return;
                    }
                }
            }
        });
        pool = null; // allows it to be gc-able
        while (true) {
            Thread.sleep(1000);  // need to handle exception here
            System.gc();         // pull the big GC handle
        }
    }
}

【讨论】:

  • 是的,我怀疑你会这么说。工人没有暴露(AFAIK),所以除了执行者本身之外,没有任何东西可以持有对工人的引用。这个循环引用与 GC 无关,一旦对 executor 的引用丢失,两者仍然有资格被删除。
  • 但如果Thread 正在运行@Michael,则不会。如果线程正在运行,那么它就是一个 GC“根”。该根具有对 TPE 的引用。
  • 我不明白这应该如何解决我刚才所说的问题。您声称由于嵌套类持有的引用而保留了执行程序。那是虚假的推理。如果这是真的,那么任何包含嵌套类的类都不会被 GC。
  • 如果有对嵌套类的引用,那么是的,非静态外部类永远不会被 GC'd。线程是 GC 根,对吗?线程仍在运行。线程的堆栈帧包括 Worker 和 TPE 实例。我认为这不可能是 GC'd @Michael。
  • 澄清一下:从worker到executor的引用本身并不是问题。问题是 JVM(某处)有对其运行线程的引用,并且这些引用在(至少)线程完成之前不会被清除。从 worker 到 executor 的引用很重要,因为它创建了从 JVM 到 executor 的路径。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多