executorService.submit(Runnable)返回的未来对象是否持有对可运行对象的引用?

14

假设我们有以下代码:

List<Future<?>> runningTasks;
ExecutorService executor;
...
void executeTask(Runnable task){
    runningTasks.add(executor.submit(task));
}

我的问题是:

  1. runningTasks 是否持有对 task 对象的引用?
  2. 它会持有多久?任务完成后是否仍然持有它?
  3. 为避免内存泄漏,我是否需要注意删除已添加到列表中的未来对象?

1
通常是这样的。只要任务正在运行,它就不重要了,因为它已经被执行线程引用了。在任务完成后,我只需要从名为“runningTasks”的列表中删除Future即可... - Holger
我能否将列表改为保存弱引用?类似于List<WeakReference<Future<?>>>这样的形式? - Daniel Rusev
你可以这样做。但是这会让我想知道为什么你首先要将“Future”存储在列表中。 - Holger
因为在某些时候我需要取消某些任务。 - Daniel Rusev
然后,List<WeakReference<Future<?>>> 就可以工作了。它将允许 Future 被垃圾回收,但您必须手动删除 WeakReference 实例(尽管 WeakReference 本身不占用太多空间)。另一种选择是使用 Collections.newSetFromMap(new WeakHashMap<Future<?>,Boolean>()) 创建一个 Set<Future<?>>,它允许其元素被垃圾回收。再也没有更简单的方法了... - Holger
2个回答

2

直到执行器或Future对象保留对其的引用是一项实现细节。因此,如果您的任务使用了大量内存,以至于需要担心内存使用情况,则应在任务完成之前显式清理任务中的内存。

如果您查看ThreadPoolExecutorOpenJDK 1.6源代码,实际上可以看到底层Future对象无限期地保留对底层可调用对象的引用(即只要Future对象有强引用,就不能对可调用对象进行垃圾回收)。这也适用于1.7版本。从1.8开始,对其的引用被置为空。但是,您无法控制任务将在哪个ExecutorService实现上运行。

在实践中,使用WeakReference应该有效,因为Future和因此Callable对象可以在任务完成后进行垃圾回收,并且合理的ExecutorService实现应该在任务完成时失去对它的引用。严格来说,这仍取决于ExecutorService的实现。此外,使用WeakReference可能会增加意外的大开销。如果您只是显式清理占用大量内存的对象,那么效果会更好。相反,如果您没有分配大型对象,则不必担心。

当然,这个讨论与在列表中保留未删除任何内容的future所导致的内存泄漏完全不同。即使使用WeakReference也无济于事;您仍将面临内存泄漏问题。为此,请简单地遍历列表并删除已完成且无用的futures。每次执行此操作都是可以的,除非队列大小非常大,因为这非常快速。


在ThreadPoolExecutor中,我没有看到任何地方Future对象无限期地保留对底层可调用对象的引用。 - Vipin
@Vipin:ThreadPoolExecutor返回一个FutureTask实例作为Future。这个FutureTask实现又有一个字段FutureTask.Sync.callable,它指向底层的callable。持有同步对象的字段和指向底层callable的字段都是final的。请参见->http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/concurrent/FutureTask.java#FutureTask.Sync.%3Cinit%3E%28java.util.concurrent.Callable%29 - Enno Shioji
在JDK 7中,它将引用存储在FutureTask的可调用引用变量中,但是如果您查看FutureTask.finishCompletion()方法,则会将其设置为null。finishCompletion()方法在cancel()和run()中都被调用,因此最终它变成了null,没有内存泄漏的危险。 - Vipin
@Vipin:你说的是哪个JDK?我正在看OpenJDK,也许你在看Oracle JDK或IBM JDK?此外,它可能在次要版本中发生了变化,或者在未来版本(如JDK 9)中发生变化。其他ExecutorServices(如Netty的MemoryAwareExecutor或Guava的ListeningExecutorservice)会发生什么?你无法知道所有实现的行为。这就是为什么你不应该依赖于实现的巧合。 - Enno Shioji
我只是举了FutureTask实现的例子,这个实现是由Doug Lea和他的团队编写的。同样的实现也被用在OpenJDK和Oracle JDK中。我正在使用Oracle JDK 1.7.0_60版本。 - Vipin

-1
1. 运行 runningTasks(Future) 是否会持有对任务对象的引用? --- 是 2. 它会持有多长时间?任务完成后它仍然持有吗? --- 它会持有任务引用直到任务完成,之后它将从 ThreadPoolExecutor 的队列中移除。在 FutureTask.finishCompletion() 方法中,我们将可调用 (callable) (task) 的引用设置为 null。FutureTask.finishCompletion() 在 FutureTask 的 run 和 cancel 方法中被调用。因此,在运行和取消两种情况下,future 都不会持有任务的引用。 3. 为了避免内存泄漏,我是否需要注意删除添加到列表中的 future?--- 如果您使用 Future,则是安全的。
如果您使用 ScheduledFuture,则可能会遇到内存泄漏问题,因为通常情况下 ScheduledFuture.cancel() 或 Future.cancel() 不会通知其 Executor 已被取消,并且它会留在队列中直到它的执行时间到来。对于简单的 Futures 来说这并不是什么大问题,但对于 ScheduledFutures 来说可能是个大问题。它可能会停留几秒钟、几分钟、几小时、几天、几周、几年或几乎无限期,具体取决于它被安排的延迟时间。

如需更多详细信息,包括内存泄漏情况及其解决方案示例,请参阅我的其他答案


我不认为这是正确的。至少在OpenJDK 1.6中,ThreadPoolExecutor创建的Future对象在任务完成后仍会保留对底层可调用对象的引用。 - Enno Shioji
@EnnoShioji,请查看ThreadPoolExecutor.Worker类的getTask()方法。它包含poll()和take()调用,用于检索并删除队列的头部。 - Vipin
当然,它会从内部队列中删除,但是Future对象(即FutureTask)仍具有指向底层可调用对象的字段,如此处所示:http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/concurrent/FutureTask.java#FutureTask.cancel%28boolean%29 - Enno Shioji
该字段是FutureTask.Sync.callable。 - Enno Shioji
在OpenJDK 1.7中,它仍然存在,在1.8中似乎开始将其置零(http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8-b132/java/util/concurrent/FutureTask.java)。然而,这正是为什么您不希望依赖于实现细节的原因。 - Enno Shioji
在JDK 7中,FutureTask在callable引用变量中存储引用。但是,如果您查看FutureTask.finishCompletion()方法,它将被设置为null。finishCompletetion方法在cancel()和run()中都会被调用,因此最终它会变成null,不会造成内存泄漏的危险。 - Vipin

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