听众作为弱点的利弊

时间:2011-06-14 00:02:32

标签: java design-patterns observer-pattern weak-references

将听众保持为WeakReferences的优点和缺点是什么。

当然,最重要的'职业'是:

将侦听器添加为WeakReference意味着侦听器不需要打扰“删除”本身。

更新

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

那些不关心搬迁的人可以使用后者。

12 个答案:

答案 0 :(得分:69)

首先,在侦听器列表中使用WeakReference将为您的对象提供不同的语义,然后使用硬引用。在硬引用的情况下,addListener(...)表示“通过removeListener(..)”将“明确停止”通知所提供的对象,在弱引用的情况下,它意味着“通知提供的对象有关特定事件,直到此对象不被其他任何人使用(或明确停止使用removeListener)“。请注意,在许多情况下,拥有对象,侦听某些事件以及没有其他引用来保留GC是完全合法的。记录器就是一个例子。

正如你所看到的,使用WeakReference不仅解决了一个问题(“我应该记住不要忘记在某个地方删除添加的监听器”),还要提出另一个问题 - “我应该记住,我的听众可以停止在没有任何参考的时候随时倾听“。你没有解决问题,你只是将一个问题换成另一个问题。看,以任何方式你被迫清楚地定义,设计和追踪你的听众的活跃 - 无论如何。

所以,就个人而言,我同意提及在听众列表中使用WeakReference更像是一个黑客而不是一个解决方案。例如,值得了解的模式,有时它可以帮助您 - 使遗留代码运行良好。但这不是选择的模式:)

P.S。此外,应该注意WeakReference引入了额外的间接级别,在某些情况下,事件率极高,会降低性能。

答案 1 :(得分:31)

这不是一个完整的答案,但你引用的力量也可能是它的主要弱点。考虑如果动作侦听器被弱化地实现会发生什么:

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

该动作监听器随时都会收集垃圾!对匿名类的唯一引用是您要添加它的事件并不罕见。

答案 2 :(得分:10)

我看过很多代码,其中侦听器未正确注册。这意味着他们仍然被不必要地执行不必要的任务。

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

不使用WeakReference相当于冒着消耗不必要的内存和CPU的风险。它更复杂,更复杂,需要在复杂代码中使用硬引用进行更多工作。

WeakReferences充满了优点,因为它们会自动清理。唯一的问题是你不能忘记在代码中的其他地方保留一个硬引用。通常情况下,依赖于此侦听器的对象会出现这种情况。

我讨厌创建侦听器的匿名类实例的代码(如Kirk Woll所述),因为一旦注册,就不能再注销这些侦听器了。您没有对它们的引用。编码恕我直言,这是非常糟糕的。

当您不再需要时,您还可以null对听众的引用。您不必再担心它了。

答案 3 :(得分:5)

真的没有专业人士。弱参数通常用于“可选”数据,例如您不希望阻止垃圾回收的缓存。您不希望收集侦听器垃圾,您希望它继续收听。

更新:

好的,我想我可能已经弄清楚你得到了什么。如果要向长期存在的对象添加短期监听器,则使用weakReference可能会有好处。因此,例如,如果您要将PropertyChangeListeners添加到域对象以更新不断重新创建的GUI的状态,则域对象将保留到可能构建的GUI。想象一下不断重新创建的大弹出对话框,通过PropertyChangeListener将侦听器引用回Employee对象。如果我错了,请纠正我,但我认为整个PropertyChangeListener模式不再受欢迎了。

另一方面,如果你正在谈论GUI元素之间的监听器或者让域对象听GUI元素,你将不会购买任何东西,因为当GUI消失时,听众也会这样做。

以下是一些有趣的读物:

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

How to resolve swing listener memory leaks?

答案 4 :(得分:4)

说实话,我并不真正购买这个想法,而是你对addWeakListener的期望。也许这只是我,但它似乎是一个错误的好主意。起初它是诱惑但它可能暗示的问题不可忽视。

使用weakReference,您不确定在不再引用侦听器本身时将不再调用侦听器。垃圾收集器可以在几毫秒后或从不释放存储器。这意味着它可能会继续消耗CPU并使其变得奇怪,就像抛出异常一样,因为不应该调用侦听器。

使用swing的一个示例是尝试执行只有在UI组件实际附加到活动窗口时才能执行的操作。这可能会抛出异常,并影响通知程序使其崩溃并阻止有效的侦听器被记录。

已经说过的第二个问题是匿名听众,他们很快就会被释放,从来没有被通知过,也没有被通知过几次。

您尝试实现的目标很危险,因为当您停止接收通知时,您无法再进行控制。它们可能永远持续或过早停止。

答案 5 :(得分:3)

因为您正在添加WeakReference侦听器,我假设您正在使用自定义Observable对象。

在以下情况下对对象使用WeakReference非常有意义。 - Observable对象中有一个监听器列表。 - 您已经在其他地方对听众进行了硬性引用。 (你必须确定这一点) - 您不希望垃圾收集器停止清除侦听器,因为Observable中有对它的引用。 - 在垃圾收集期间,将清除听众。在通知侦听器的方法中,从通知列表中清除WeakReference对象。

答案 6 :(得分:2)

我想不出任何使用WeakReferences用于侦听器的合法用例,除非您的用例涉及在下一个GC周期后明确不应存在的侦听器(当然,该用例将是VM / platform特异性的)。

可以设想一个稍微更合理的SoftReferences用例,其中侦听器是可选的,但占用了大量的堆,应该是免费堆大小开始变得冒险的第一个。我想,某种可选的缓存或其他类型的辅助监听器可能是候选者。即使这样,你似乎也希望听众的内部使用SoftReferences,而不是听众和听众之间的联系。

通常,如果您使用的是持久侦听器模式,则侦听器是非可选的,因此提出此问题可能是您需要重新考虑架构的症状。

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

答案 7 :(得分:2)

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

在实践中,我看到很多软件会让听众永远保持。程序员通常不会意识到他们应该取消注册它们。

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

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

此代码将注册侦听器,返回一个封装侦听器的对象,并具有一个方法keepFor,它将侦听器添加到静态weakHashMap中,并将instance参数作为键。这将保证监听器至少在someInstance和其他实例不被垃圾收集时注册。

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

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

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

编辑:创建了一个小型lib,它实现了这个aproach https://github.com/creichlin/struwwel

的基本版本

答案 8 :(得分:1)

我对原始海报有3条建议。很抱歉复活旧线程,但我认为此解决方案之前没有讨论过我的解决方案。

首先, 请考虑使用JavaFX库中的javafx.beans.values.WeakChangeListener示例。

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

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

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

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

为了尊重那些规定“总是取消订阅你的听众的良好编程习惯”的人,每当一个Listener使用取消订阅时,我确保创建一个日志条目并在稍后的代码中纠正问题。

答案 9 :(得分:0)

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

如前所述,与通常的addListener / removeListener情况相比,这确实是不同的语义,但在某些情况下它是有效的。

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

这里,弱侦听器使子节点可以监听父节点,同时也可以通过可达性/缓存来确定它们的生命周期,因此结构不会将所有隐含节点保留在内存中。

答案 10 :(得分:0)

如果您在某个无法保证每次调用的地方取消注册,您可能还需要使用WeakReference实现您的侦听器。

我似乎记得我们的ListView中的行视图中使用的一个自定义PropertyChangeSupport侦听器存在一些问题。我们找不到一个好的和可靠的方法来取消注册那些监听器,所以使用WeakReference监听器似乎是最干净的解决方案。

答案 11 :(得分:0)

从测试程序看来,匿名ActionListeners不会阻止对象被垃圾回收:

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