听者作为弱引用的利与弊

保留侦听器作为弱引用的利弊是什么?

当然,最大的“优点”是:

添加侦听器作为 WeakReference 意味着侦听器不需要自己“删除”。

对于那些担心侦听器只有对象的引用的人来说,为什么不能有两个方法 addListener ()和 addWeakRefListener ()呢?

那些不关心移除的人可以使用后者。

18619 次浏览

这并不是一个完整的答案,但是你提到的优点也可能是它的主要缺点。考虑一下,如果动作侦听器的实现不足,会发生什么情况:

button.addActionListener(new ActionListener() {
// blah
});

那个动作监听器随时都会被垃圾收集!对匿名类的唯一引用是要将其添加到的事件,这种情况并不少见。

真的没有优点。弱引用通常用于“可选”数据,比如不希望阻止垃圾回收的缓存。你不希望你的听众被垃圾收集,你希望他们继续听下去。

更新:

好吧,我想我知道你想说什么了。如果您要向长期存在的对象添加短期侦听器,那么使用弱引用可能会有好处。例如,如果您将 PropertyChangeListener 添加到域对象中,以更新不断重新创建的 GUI 的状态,那么域对象将保留 GUI,这可能会积累起来。想象一个不断重新创建的大型弹出对话框,其中一个侦听器引用通过 PropertyChangeListener 返回到 Employee 对象。如果我说错了请纠正我,但是我不认为 PropertyChangeListener 模式是非常流行的。

另一方面,如果您讨论的是 GUI 元素之间的监听器,或者让域对象监听 GUI 元素,那么您不会购买任何东西,因为当 GUI 消失时,监听器也会消失。

下面是一些有趣的阅读材料:

Http://www.javalobby.org/java/forums/t19468.html

如何解决摆动侦听器内存泄漏?

我想不出对侦听器使用 WeakReferences 的任何合法用例,除非您的用例涉及到在下一个 GC 周期之后显式不应该存在的侦听器(当然,这个用例是特定于 VM/平台的)。

我们可以设想一个稍微合法一些的 SoftReferences 用例,其中侦听器是可选的,但是会占用大量堆,当自由堆大小开始变得危险时,应该首先使用它。某种可选的缓存或其他类型的辅助侦听器,我想,可能是一个候选者。即使这样,您似乎也希望听众的内部使用 SoftReferences,而不是听众和听众之间的链接。

通常,如果您使用的是持久侦听器模式,那么侦听器是非可选的,因此问这个问题可能是您需要重新考虑架构的征兆。

这是一个学术问题,还是你有一个实际的情况要解决?如果这是一个实际的情况,我想听听它是什么-你可能会得到更多,少抽象的建议,如何解决它。

我见过大量的代码,其中侦听器没有被正确地注册。这意味着它们仍然被不必要地调用来执行不必要的任务。

如果只有一个类依赖于一个侦听器,那么清理起来很容易,但是如果有25个类依赖于它,会发生什么情况呢?正确地注销它们变得更加棘手。事实上,您的代码可以从一个引用侦听器的对象开始,并在将来的版本中使用25个引用同一个侦听器的对象结束。

不使用 WeakReference相当于承担消耗不必要的内存和 CPU 的巨大风险。它更加复杂、棘手,并且需要在复杂代码中进行更多的硬引用工作。

WeakReferences充满了优点,因为它们是自动清理的。唯一的缺点是您不能忘记在代码的其他地方保留硬引用。通常,在依赖这个侦听器的对象中会这样做。

我讨厌创建侦听器的匿名类实例的代码(正如 Kirk Woll 所提到的) ,因为一旦注册,就不能再注销这些侦听器。你没有提到他们。恕我直言,编码真的很糟糕。

当你不再需要它的时候,你也可以把 null作为一个监听器的引用。你不用再担心了。

老实说,我并不真的相信这个想法,也不知道您希望使用 addWeakListener 做什么。也许这只是我的想法,但这似乎是一个错误的好主意。起初,它是诱人的,但它可能暗示的问题不容忽视。

使用弱引用时,您不能确定当不再引用侦听器本身时,将不再调用该侦听器。垃圾收集器可以在几毫秒之后释放内存,也可以永远不释放内存。这意味着它可能会继续消耗 CPU,这就像抛出异常一样奇怪,因为不应该调用侦听器。

秋千的一个例子是尝试做一些事情,只有当你的 UI 组件实际上连接到一个活动窗口时你才能做。这可能会引发异常,并影响通知器使其崩溃并阻止通知有效的侦听器。

第二个问题已经说明是匿名侦听器,他们可能很快被释放,从来没有通知或只有几次。

您正在尝试实现的目标是危险的,因为当您停止接收通知时,您将无法再进行控制。它们可能会永远持续下去,也可能很快就会停止。

因为您正在添加 WeakReference 侦听器,所以我假设您正在使用一个自定义的 Observer 对象。

在下面的情况下,使用 WeakReference 指向对象是非常有意义的。 - 在可观察对象中有一个监听器列表。 - 你已经有一个硬参考,听众在其他地方。(你必须确定这一点) - 您不希望垃圾收集器停止清除侦听器,仅仅因为在观察中有对它的引用。 - 在垃圾收集期间,听众将被清理干净。在通知侦听器的方法中,清除通知列表中的 WeakReference 对象。

首先,在侦听器列表中使用 WeakReference 将给对象不同的 语义上的,然后使用硬引用。在硬引用情况下,addListener (...)表示“通知提供的对象关于特定事件 直到我明确阻止它和 RemoveListener (。.)"在弱引用情况下,它意味着“通知提供的对象关于特定事件 直到这个对象不会被其他任何人使用(或显式地停止 RemoveListener)”。注意,在许多情况下,拥有对象、监听某些事件并且没有其他引用阻止对象访问 GC 是完全合法的。Logger 可以是一个例子。

正如你所看到的,使用 WeakReference 不仅仅解决了一个问题(“我应该记住不要忘记删除添加的监听器”) ,而且还提出了另一个问题——“我应该记住,我的监听器可以在任何时候停止监听,当没有任何参考了”。你不解决问题,你只是用一个问题交换另一个问题。看,无论如何你已经强迫清楚地定义,设计和跟踪你的听众的寿命-这样或那样。

因此,就我个人而言,我同意提到在侦听器列表中使用 WeakReference 更像是一种破解,而不是一种解决方案。这是一种值得了解的模式,有时它可以帮助您——例如,使遗留代码工作得很好。但这不是一种选择模式:)

另外,还应该注意 WeakReference 引入了额外的间接级别,在某些情况下,这种间接级别的事件率极高,可能会降低性能。

在您特别希望 GC 控制侦听器的生命周期的情况下,WeakListener 非常有用。

如前所述,与通常的 addListener/RemoveListener 案例相比,这确实是不同的语义,但在某些场景中是有效的。

例如,考虑一个非常大的树,它是稀疏的——一些节点级别没有显式定义,但是可以从层次结构更高的父节点推断出来。隐式定义的节点监听那些已定义的父节点,以使它们的隐含/继承值保持最新。但是,这个树是巨大的——我们不希望隐含的节点永远存在——只要它们被调用代码使用,再加上一个可能需要几秒钟的 LRU 缓存,以避免重复搅动相同的值。

在这里,弱侦听器使子节点可以侦听父节点,同时也可以通过可达性/缓存决定它们的生存期,这样结构就不会维护内存中的所有隐含节点。

如果要在某个不能保证每次都调用的地方注销弱引用,那么可能还需要使用 WeakReference 实现侦听器。

我似乎还记得,我们在 ListView 的行视图中使用的一个定制 PropertyChangeSupport侦听器出现了一些问题。我们无法找到一种好的、可靠的方法来注销这些侦听器,因此使用 WeakReference 侦听器似乎是最干净的解决方案。

从测试程序中可以看出,匿名 ActionListener 不会阻止对象被垃圾收集:

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;


import javax.swing.JButton;


public class ListenerGC {


private static ActionListener al = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.err.println("blah blah");
}
};
public static void main(String[] args) throws InterruptedException {


{
NoisyButton sec = new NoisyButton("second");
sec.addActionListener(al);
new NoisyButton("first");
//sec.removeActionListener(al);
sec = null;
}
System.out.println("start collect");
System.gc( );
System.out.println("end collect");
Thread.sleep(1000);
System.out.println("end program");
}


private static class NoisyButton extends JButton {
private static final long serialVersionUID = 1L;
private final String name;


public NoisyButton(String name) {
super();
this.name = name;
}


@Override
protected void finalize() throws Throwable {
System.out.println(name + " finalized");
super.finalize();
}
}
}

生产:

start collect
end collect
first finalized
second finalized
end program

在我看来,这在大多数情况下是个好主意。负责释放侦听器的代码位于注册侦听器的同一位置。

在实践中,我看到很多软件,这是永远保持听众。程序员通常甚至没有意识到他们应该注销它们。

通常可以返回一个带有对侦听器的引用的自定义对象,该引用允许操作何时取消注册。例如:

listeners.on("change", new Runnable() {
public void run() {
System.out.println("hello!");
}
}).keepFor(someInstance).keepFor(otherInstance);

这段代码将注册侦听器,返回一个封装侦听器的对象,并且具有一个方法 keepFor,该方法将侦听器添加到一个静态弱 HashMap 中,并将实例参数作为键。这将保证侦听器被注册,至少在有些实例和其他实例没有被垃圾收集的情况下。

还可以有其他方法,比如 keepForever ()或 keepUntilCalled (5)或 keepUntil (DateTime.now () . plusSecons (5))或 unregisterNow ()。

默认值可以永久保留(直到未注册)。

这也可以在没有弱引用的情况下实现,但是没有触发删除侦听器的虚引用。

编辑: 创建了一个小库,实现了这种方法的基本版本 https://github.com/creichlin/struwwel

我对原版海报有三点建议。很抱歉重新启用了一个旧的线程,但我认为我的解决方案以前没有在这个线程中讨论过。

首先, 请考虑以 JavaFX 库中的 JavaFX.beans.values. WeakChangeListener 为例。

其次, 我通过修改我的 Observer 的 addListener 方法来升级 JavaFX 模式。新的 addListener ()方法现在为我创建相应的 WeakXxxListener 类的实例。

很容易修改“ fire event”方法,以取消对 XxxWeakListener 的引用,并在 WeakReference.get ()返回 null 时删除它们。

由于我需要迭代整个列表,因此删除方法现在变得有点讨厌,这意味着我需要进行同步。

第三, 在实施这个策略之前,我采用了一种您可能会发现有用的不同方法。(硬参考)听众得到一个新的事件,他们做了一个现实检查,他们是否仍在使用。如果没有,那么他们从允许他们 GCed 的观察员那里取消订阅。对于订阅了长期观察的短期侦听器,检测过时是相当容易的。

为了尊重那些规定“总是取消订阅您的侦听器是很好的编程实践,每当侦听器采取取消订阅本身时,我确保创建一个日志条目,并在以后的代码中纠正这个问题。