在Java中,一个正在运行的线程是否会防止其所在对象被垃圾回收?

10

给定以下代码:

 new Thread(new BackgroundWorker()).start();

直觉上感觉BackgroundWorker实例应该在线程退出之前安全地避免GC,但是否如此?为什么?

编辑:

所有这些争论基本上都是因为我在同一篇文章中提出了至少两个不同的问题。标题中的问题有一个答案,代码示例引导进入不同的方向 - 具体取决于内联。

发布的答案真的很好。我将授予Software Monkey绿色复选框。请注意,Darron的答案同样有效,但是Software Monkey解释了我遇到的问题; 这是对我有效的答案。

感谢所有人让这成为一个难忘的事件;)


你应该考虑将采纳答案改为Darron的。Software Monkey的回答实际上是错误的,而Darron则扩展了他的回答并进行了解释。 - Craig P. Motlin
天啊。愚者问的问题,十位智者也解答不了。Motlin;我需要思考!稍后回复你;) - krosenvold
@Krosenvold:我认为按照编码方式,在Thread中会保留一个引用(Darron说这必须通过契约实现)。但即使按照Darron的建议进行编码,也没有关系——即使该对象被GC'd,您的构造仍然是安全的,因为如果它是,则run()不能使用该对象。 - Lawrence Dol
6个回答

12
是的,因为GC只能收集任何线程都不可达的对象,而Thread必须持有对其可运行对象的引用(否则它将无法调用它)。因此,在您的线程正在运行时,您的可运行对象显然是可达的。
无论执行所需的语义如何,只要该对象被这个新线程或任何其他线程仍然可访问,它就不会被GC处理;至少在足以调用您的Runnable的run()方法的时间内,并且整个线程的生命周期如果该线程能够访问Runnable实例,则该构造由JVM规范保证安全。
编辑:由于Darron一直在深入探讨这个问题,并且有些人似乎被他的论点所说服,我将根据他的想法扩展解释。假设暂时不允许除Thread本身之外的任何人调用Thread.run()。在这种情况下,Thread.run()的默认实现可能如下所示:
void run() {
    Runnable tmp = this.myRunnable;  // Assume JIT make this a register variable.
    this.myRunnable = null;          // Release for GC.
    if(tmp != null) {
        tmp.run();         // If the code inside tmp.run() overwrites the register, GC can occur.
        }
    }

我认为在这种情况下,tmp仍然是对由Thread.run()中执行的线程可达的runnable的引用,因此不适合进行垃圾回收。

如果代码看起来像下面这样会怎样(出于某些难以理解的原因):

void run() {
    Runnable tmp = this.myRunnable;  // Assume JIT make this a register variable.
    this.myRunnable = null;          // Release for GC.
    if(tmp != null) {
        tmp.run();         // If the code inside tmp.run() overwrites the register, GC can occur.
        System.out.println("Executed runnable: "+tmp.hashCode());
        }
    }

很明显,在tmp.run()执行期间,被tmp引用的实例是不能进行垃圾回收的。

我认为达伦误以为可达只指可以通过从所有Thread实例开始追踪实例引用来找到的引用,而不是由任何执行线程都可以看到的引用。或者我错误地认为相反。

此外,达伦可以假设JIT编译器可以进行任何他想要的更改-编译器不允许更改执行代码的引用语义。如果我编写具有可达引用的代码,编译器不能优化掉该引用并在该引用处于作用域内时导致我的对象被收集。

我不知道如何实际寻找可达对象的详细信息;我只是推断出我认为必须保持的逻辑。如果我的推理不正确,则在方法中实例化并仅分配给该方法中的本地变量的任何对象都将立即符合进行垃圾回收的条件-显然这是不可能的。

此外,整个辩论都是无关紧要的。如果唯一的可达引用在Thread.run()方法中,因为runnable的run()方法没有引用它的实例,并且除了传递给run()方法的隐式this(在字节码中,而不是作为声明的参数)之外不存在对实例的其他引用,则是否收集对象实例并不重要 - 按照定义,这样做不会造成任何伤害,因为如果this被优化掉,则不需要它来执行代码。既然如此,即使达伦是正确的,在最终实践结果上,OP构想的结构也是完全安全的。无论哪种情况,都没有关系。让我再说一遍,以确保清楚 - 最终分析中这没关系


+1 给你,mmyers。我不知道为什么有人给你负评。我们都说了同样的话。Tom在这里并没有真正回答问题。 - Craig P. Motlin
-1是因为完全错误的原因而得出的正确答案。Darron,你获得了+1。 - Craig P. Motlin
@Darron:无论如何,从实际角度来看这都是无意义的——如果没有对可运行对象的其他引用(在OP的结构中确实没有),并且run()方法不访问该对象,则它是否被GC回收都无关紧要。 - Lawrence Dol
@Darron:明确一下,在你的例子中,Thread.run方法通过变量“local”引用了可运行对象;该引用对于线程在local.run()调用期间至少是可见的。 - Lawrence Dol
@CodeMonkey:变量“local”在其使用之外不必存在。如果JIT/优化器可以证明不需要再次使用local的值,则可以将该存储重新用于其他变量。 - Darron
显示剩余20条评论

5

它是安全的。 JVM会保留对每个线程的引用。线程保留对其构造函数中传递的Runnable实例的引用。因此,该Runnable是强可及的,并且在线程的生命周期内不会被回收。

我们知道Thread持有对runnable的引用,因为Thread.run()的javadoc如下所示:

如果这个线程是使用单独的Runnable run对象构造的,则调用该Runnable对象的run方法;否则,此方法不执行任何操作并返回。


任何强可达的对象都不会被垃圾回收。有什么部分不清楚的吗? - Craig P. Motlin

5

是的,它是安全的。原因并不像你想象的那样显而易见。

仅仅因为BackgroundWorker中的代码正在运行,并不意味着它是安全的——所讨论的代码可能实际上没有引用当前实例的任何成员,从而允许优化掉"this"。

然而,如果您仔细阅读java.lang.Thread类的run()方法规范,您会发现Thread对象必须保留对Runnable的引用才能履行其合约。

编辑:由于我在这个答案上被投了几次反对票,我将进一步解释我的解释。

假设目前除Thread本身以外的任何人都不能调用Thread.run(),

在这种情况下,Thread.run()的默认实现可以看起来像:

void run() {
    Runnable tmp = this.myRunnable;  // Assume JIT make this a register variable.
    this.myRunnable = null;          // Release for GC.
    if (tmp != null)
        tmp.run();         // If the code inside tmp.run() overwrites the register, GC can occur.
}

我一直在说的是,JLS中没有任何内容阻止对象因线程执行实例方法而被垃圾回收。这就是使得正确处理终结器如此困难的部分。
有关此问题的详细信息,请参见并发兴趣列表中此thread讨论中比我更了解此问题的人们的描述。

很明显。如果此线程是使用单独的Runnable run对象构建的,则调用该Runnable对象的run方法;否则,此方法不执行任何操作并返回。 - Craig P. Motlin
@Motlin 是的,现在我同意这是完全明显的 ;) - krosenvold
规范并不阻止您随时再次调用run()。因此,必须保留引用。 - Darron
@Tom 通常情况下你不会直接调用run()方法,否则你需要构建一个Thread并将其作为Runnable使用。但是Thread.start()方法会调用Thread.run()方法。 - Craig P. Motlin
1
@CodeMonkey:这不仅是我的假设,也是GC中顶尖大脑的推理。一个永远不会再被使用的局部变量可以被优化掉。 - Darron
显示剩余3条评论

1

是的,因为Thread在内部保留了对Runnable的引用(毕竟它需要知道要运行什么)。


但是浅显的阅读可能会认为它只需要引用可运行的内容足够长的时间来第一次调用它。 - Darron
@Darron:第一次?你不能多次启动一个线程;或者我也没有理解你的评论吗? - Michael Myers
线程只会启动一次,但您可以额外调用Thread.run()。这是一种可能性,它迫使线程对象记住在Thread.start()之后的Runnable。 - Darron
@达伦:好的,刚开始听起来你好像在说线程不需要保留它。 - Michael Myers

1

我愿意打赌JVM在其根集中包含对每个活动或可调度线程对象的引用,但我没有规范文件来确认这一点。


确实如此。Thread和ThreadGroup类都有方法,可以让您获取对当前所有正在运行的线程的引用。 - Darron
线程对象不需要保持对可运行对象的引用。 - Tom Hawtin - tackline
@Tom - 是的,它必须这样做。因为可以合法地多次调用Thread.run()方法。 - Darron

0

不,我认为这是不安全的。

实际上,你几乎肯定可以逃脱惩罚。然而,Java内存模型是令人惊讶的。事实上,就在上周,JMM邮件列表上还讨论了添加一种“保持对象活动”的方法的计划。目前,finalizer可以在没有成员方法执行的happens-before关系的情况下运行。目前,你需要通过到处同步或在每个方法末尾编写一些volatile并在finalizer中读取该volatile来引入happens-before关系。

正如Darron指出的那样,如果你可以通过Thread.enumerate(例如)获取Thread对象,则可以调用它的run方法,该方法调用Runnable的run方法。但是,我仍然认为其中没有happens-before关系。

我的建议:不要试图太“聪明”。


哇...我本来想抱怨没有评论就被踩了,但是看到这个之后,我觉得我应该被踩。 :) - Michael Myers
我至少取消了一个反对票...你的第一句话是错误的,因为有微妙的原因。但你的其余评论绝对正确且重要。 - Darron
现在你已经纠正了第一个语句。但我怀疑“发生在之前”在这里并不重要。因为你可以访问到Runnable的引用,所以必须存在一条强可达路径到它,它不能被GC。 - Darron
我也感觉这个答案比我能轻松理解的深了一点。但是,考虑到我的修正后的当前理解,我现在认为在无法访问的代码中运行线程是有明显可能性的,这似乎就是Tom所说的内容? - krosenvold

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