executorService.submit(Runnable) 返回的未来对象是不是包含对可运行对象的任何引用?

Posted

技术标签:

【中文标题】executorService.submit(Runnable) 返回的未来对象是不是包含对可运行对象的任何引用?【英文标题】:Does the future object returned by executorService.submit(Runnable) hold any reference to the runnable object?executorService.submit(Runnable) 返回的未来对象是否包含对可运行对象的任何引用? 【发布时间】:2014-05-25 17:28:18 【问题描述】:

假设我们有以下代码:

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

我的问题是:

    runningTasks 是否持有对task 对象的引用? 它能保存多长时间?任务完成后还持有吗? 为了避免内存泄漏,我是否必须小心删除添加到列表中的未来?

【问题讨论】:

通常,是的。只要任务正在运行,这并不重要,因为无论如何它都会被执行线程引用。完成后,我只需从名为 runningTasks 的列表中删除 Future... 我可以将列表改为保存弱引用吗?类似 List>>? 你可以的。但这会让我想知道您为什么首先将Futures 存储在列表中。 因为在某些时候我需要取消某些任务。 然后,List&lt;WeakReference&lt;Future&lt;?&gt;&gt;&gt; 将起作用。它将允许 Futures 被 gc'ed 但您必须手动删除 WeakReference 实例(尽管 WeakReference 本身并不占用太多空间)。另一种方法是Collections.newSetFromMap(new WeakHashMap&lt;Future&lt;?&gt;,Boolean&gt;()) 创建一个Set&lt;Future&lt;?&gt;&gt;,它允许其元素被gc'ed。再简单不过了…… 【参考方案1】:
    runningTasks(Future) 是否持有对任务对象的引用? --- 是的 它能保存多长时间?任务结束后还持有吗 做完了? --- 它持有任务引用,直到任务完成,之后它从 ThreadPoolExecutor 的队列中删除,也在 FutureTask.finishCompletion() 方法中我们将 callable(task) 引用设置为 null。 FutureTask.finishCompletion() 在 FuturetTask 的 run 和 cancel 方法中被调用。因此,在 run 和 cancel 两种情况下,future 都没有对任务的引用。 为了避免内存泄漏,我是否必须小心删除添加到列表中的未来? --- 如果你使用Future,你是安全的。

如果您使用ScheduledFuture,那么您可能会面临内存泄漏问题,因为ScheduledFuture.cancel()Future.cancel() 通常不会通知它的Executor 它已被取消并且它会停留在队列中,直到其执行时间结束到达的。对于简单的 Futures 来说这不是什么大问题,但对于 ScheduledFutures 来说可能是个大问题。它可以在那里停留几秒钟、几分钟、几小时、几天、几周、几年或几乎无限期,具体取决于它所安排的延迟。

有关内存泄漏情况及其解决方案的更多详细信息,请参阅我的另一个answer。

【讨论】:

我不认为这是正确的。至少在 OpenJDK 1.6 中,ThreadPoolExecutor 创建的 Future 对象即使在任务完成后仍保留对底层可调用对象的引用。 @EnnoShioji 看看 ThreadPoolExecutor.Worker 类的 getTask() 方法。它有 poll() 和 take() 调用检索和删除队列的头部。 当然,它已从内部队列中删除,但作为 FutureTask 的 Future 对象仍然有一个引用底层可调用对象的字段,如下所示:grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… 该字段是 FutureTask.Sync.callable 在 OpenJDK 1.7 中它仍然存在,而在 1.8 中他们似乎开始将其归零(grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…)。然而,这正是您不想依赖实现工件的原因。【参考方案2】:

直到执行器或Future 对象持有对它的引用是一个实现细节。因此,如果您的任务使用大量内存以至于您不得不担心内存使用情况,您应该在任务完成之前明确地清理您的任务。

如果您查看ThreadPoolExecutor 的OpenJDK 1.6 source code,您可以看到实际上底层Future 对象实际上无限期地保留了对底层可调用对象的引用(即只要有对@ 的强引用) 987654326@对象,callable不能是GCd)。 1.7 也是如此。从 1.8 开始,对它的引用 无效。但是,您无法控制您的任务将运行 ExecutorService 的哪个实现。

在实践中使用WeakReference 应该像Future 一样工作,因此一旦任务完成,Callable 对象就可以被 GCd,ExecutorService 的合理实现应该在任务完成时失去对它的引用。严格来说,这仍然取决于 ExecutorService 的实现。此外,使用 Wea​​kReference 会增加惊人的大开销。你最好简单地明确清理占用大量内存的任何对象。相反,如果您不分配大型对象,那么我不会担心。

当然,这个讨论完全不同于如果您不断将期货添加到您的列表而不删除任何期货,您将会遇到的内存泄漏。如果您这样做,即使使用 WeakReference 也无济于事;你仍然会有内存泄漏。为此,只需遍历列表并删除已经完成并因此无用的期货。每次都这样做真的很好,除非你有一个非常大的队列,因为这非常快。

【讨论】:

在 ThreadPoolExecutor 中我看不到任何地方 Future 对象无限期地保留对底层可调用对象的引用。 @Vipin:ThreadPoolExecutor 返回一个 FutureTask 实例作为 Future。这个 FutureTask 实现又具有一个字段FutureTask.Sync.callable,它指向底层的可调用对象。保存同步对象的字段及其指向底层可调用对象的字段都是最终的。见->grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… 在 JDK 7 中,它将引用存储在 FutureTask 的可调用引用变量中,但是如果您查看 FutureTask.finishCompletion() 方法,它将被设置为 null。并且在 cancel() 和 run() 中都调用了 finishCompletion,因此最终它变为 null 并且没有内存泄漏的危险。 @Vipin:你说的是哪个 JDK?我在看 OpenJDK,也许你在看 Oracle JDK 或 IBM JDK?此外,它可能在次要版本中发生了变化,或者在 JDK 9 等未来版本中发生了变化。其他 ExecutorServices 会发生什么,比如 Netty 的 MemoryAwareExecutor 或 Guava 的 ListeningExecutorservice?您无法知道所有实现的行为方式。这就是为什么您不应该依赖实施的巧合。 我仅给出了由 Doug Lea 和 Team 编写的 FutureTask 实现示例。 OpenJDK 和 Oracle JDK 使用相同的实现。我正在使用 Oracle JDK 1.7.0_60。

以上是关于executorService.submit(Runnable) 返回的未来对象是不是包含对可运行对象的任何引用?的主要内容,如果未能解决你的问题,请参考以下文章

executorService.submit(Runnable) 返回的未来对象是不是包含对可运行对象的任何引用?

为啥 ExecutorService.submit(Runnable task) 返回 Future<?> 而不是 Future<Void>?

《Java并发编程实战》第六章 任务运行 读书笔记

Guava Future

157-模拟高并发代码

双重检测机制解决缓存穿透问题