为什么Java ThreadPoolExecutor要重写finalize()方法

3
我想知道为什么ThreadPoolExecutor的finalize()方法在已知finalize方法只有在所有线程都停止后才会被GC调用时,还要调用其shutdown()方法。那么ThreadPoolExecutor为什么要重写finalize()呢?
对我来说(以及我的项目中线程泄漏的来源),ThreadPoolExecutor.finalize()调用shutdown()似乎是具有误导性的,因为这给人一种强烈(但错误)的印象: - ThreadPoolExecutor管理其线程的生命周期,并在GC收集ThreadPoolExecutor对象时停止线程 - 只有在想获得确定性结果而不是依赖于GC来清理时才需要调用shutdown()或shutdownNow()(显然,这是不好的做法!)
注: 在这个线程中,why-doesnt-this-thread-pool-get-garbage-collected Affe解释了为什么客户端仍然需要调用shutdown()。
在这个帖子中,为什么ThreadPoolexecutor.finalize()调用shutdown而不是shutdownNow,楼主对此话题感到困惑,但答案并不像1那样全面。 ThreadPoolExecutor.finalize()JavaDocs确实包含了“它没有线程”的字眼,但这很容易被忽视。

1
我在这里同意finalize()的文档有点令人困惑......我想知道一个.newFixedThreadPool()怎么可能是"没有线程"。 - fge
ThreadPoolExecutor的JavaDocs有一个Finalization部分,解释了“如果您想确保即使用户忘记调用shutdown(),也会回收未引用的池,则必须通过设置适当的保活时间、使用零核心线程的下限和/或设置allowCoreThreadTimeOut(boolean)来安排未使用的线程最终死亡。” 因此,线程计数可能会减少并达到零。 - Diarmuid
1个回答

3
首先,如果您认为这是一个错误,请向Oracle报告。
其次,我们无法明确回答您的问题,因为您实际上在问“为什么设计成这样”……而当他们做出决定时,我们并不在场。
但我怀疑原因是当前选择被认为是两害相权取其轻:
一方面,如果线程池只是因为不再直接引用而被关闭,那么正在执行真正工作的线程可能会过早地终止。
另一方面,正如您观察到的,一个不会在变得不再直接可达时自动关闭的线程池可能会导致内存泄漏。
鉴于显然有避免存储泄漏的方法,我认为第二种选择(即当前行为)是两害相权取其轻的选择。
无论如何,有明确证据表明设计师考虑过这种行为;即来自ThreadPoolExecutor javadoc的引文:

Finalization

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

(这样的答案就回应了@fge的评论。 当配置为超时的非活动工作线程时,它会发生。)

我还认为有一种基于实现的原因。 请看这里here的代码。

线程池中的每个线程都引用了一个Runnable,实际上是内部类ThreadPoolExecutor.Worker的实例。这意味着从(活动的,尽管可能空闲的)线程到ThreadPoolExecutor.Worker对象有一个强引用路径,并且从该对象到封闭的ThreadPoolExecutor实例也有一个引用路径。由于活动线程始终是可达的,因此ThreadPoolExecutor实例在其所有线程实际终止之前仍然是可达的。
现在我/我们无法告诉您行为或javadoc规范哪个先出现。请参见我上面的“第二点”...但是,正如我上面所说的(“首先...”),如果您认为这是错误,请向Oracle报告。如果您认为javadoc具有误导性,则同样适用...尽管我认为您引用的材料证据不足。
关于为什么他们过载 finalize() 来调用 shutdown(),如果在这种情况下 shutdown() 什么也不做:
  • 可能在早期版本中它会做一些重要的事情,和

  • shutdown() 方法最后调用一个钩子方法,显然在子类中被覆盖以执行一些重要操作... 根据注释。


我认为实现或JavaDocs中没有错误。例如,线程池在有任务要执行时不能停止。我的问题很简单:“为什么设计者选择覆盖finalize()”,我认为你的答案是你不知道...也就是说,你认为在GC线程上下文中调用shutdown()没有任何有用的作用。 - Diarmuid
正如帖子中所解释的那样,覆盖finalize以调用shutdown()被高级开发人员误解为使用ThreadPookExecutor的优点之一是它管理线程的生命周期。我无法反驳Oracle覆盖finalize()是一个错误,因为他们描述了“没有线程”的情况。无论如何,感谢您抽出时间来回复。 - Diarmuid
1
@Diarmuid - 我该说什么呢?也许高级开发人员只需要更仔细地阅读javadocs...特别是如果>>你<<甚至不准备说javadocs是误导性的!(注:在我看来,如果javadoc是误导性的,那就是一个bug。而说javadocs是误导性的,却不向Oracle报告,那就是推卸责任。) - Stephen C

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