作为弱引用的侦听器的优缺点


72

将侦听器保留为WeakReferences的利弊是什么?

当然,最大的“专业版”是:

将侦听器添加为WeakReference意味着侦听器无需费心“删除”自身。

更新资料

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

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



1
除了使api杂乱无章,还有两种方法可以买到什么?尽管删除所有已添加的侦听器是一种(仍然好)的风格,但通常这样做也无济于事:垃圾收集非常小心,除非在极少数情况下。如果你撞了不释放听众产生了内存泄露,认真分析形势,并做一些analoguous什么AbstractButton中其行动的的PropertyChangeListener做
克列奥帕特拉

1
如果您的听众参考能力较弱,则无需“仔细分析情况”;)
pdeva 2011年

我确实使用了弱侦听器,但没有将其用作接口的一部分,仅在内部使用,它强制保持对侦听器的外部引用并注销(匿名类可能没有该引用)。否则,将保留一个仅泄漏的参考。重要的是要记住,任何addXXXListener后面都必须带有removeXXXListener,以确保正常的生命周期。取消注册不是一种选择(除非两个对象都具有相同的生命周期范围)。该模式既可用于摆动模式,也可用于esp模式。用于服务器开发。
bestsss 2011年

1
那些无法想到WeakReference为什么有意义的人在这里有很多答案,所以我认为我至少要添加一个参考,以提及我认为它可以解决的原型问题:失效的听众问题
唐·哈奇

Answers:


73

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

如您所见,使用WeakReference不仅可以解决一个问题(“我应该记住不要忘记在某个地方删除添加的监听器”),而且还会引发另一个问题-“我应该记住,我的监听器可以在任何时候停止监听不再引用它的时刻”。您不能解决问题,而只是将一个问题换成另一个。看,您以任何一种方式必须明确定义,设计和跟踪收听者的生命周期。

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

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


3
使用WeakReference的另一个问题-“我应该记住,由我的侦听器引起的任何状态更改在超出范围后仍可能会发生”。因此,在设计WeakReference侦听器时,需要知道其存在是不确定的。
Bringer128 2011年

是的,您绝对正确。但是从我的经验来看,似乎很多情况下这不是问题,那是什么原因呢?)据我所知,即使在jdk代码中,您也可以看到对监听器使用WeakReference的示例: )
BegemoT 2011年

您是否有任何有关WeakReference的引用在jdk中用作侦听器?我很想知道它的使用位置。
Bringer128 2011年

@ Bringer128好吧,看来我错了。我已经看到了基于WR-听众的许多用途左右摇摆,但看着来源我找不到任何的例子里面。JDK使用了更有趣的技术-侦听器列表本身使用普通引用,但是具体的侦听器用作适配器,仅将WeakRef保留给实际的对象接收事件-并将其自身注销为目标GC版本。有趣的:)
BegemoT 2011年

1
不能说JDK,但是Android SDK使用一种-SharedPreference是示例之一。它使用WaekHashMap来存储注册的侦听器。grepcode.com/file/repository.grepcode.com/java/ext/… 可能是利用弱引用的命名方法,例如addWeakListener(listener)可能是一个好习惯,因为此类的用户将了解内部发生了什么事情以避免丢失通知
vir我们

32

这不是一个完整的答案,但是您引用的优势也可能是其主要缺点。考虑一下,如果动作监听器实施得很弱,会发生什么情况:

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

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


1
为什么不能有2种方法,addListener()和addWeakRefListener()?那些不关心移除的人可以使用后者
pdeva

@pdeva,是的,那将起作用。尽管人们必须记住要调用的方法,这有点危险。我认为这种模式不一定是您应该始终避免的东西,但是它最好有一个很好的理由-而且IMO认为,原因不能简单地是“防止健忘的程序员犯错误”。
Kirk Woll

1
“防止健忘的程序员犯错误。” 这不是为什么发明了GC吗?:)还可以使您的代码更简单,因为您在处理对象时不必记住删除监听器。
pdeva 2011年

6
@pdeva,我并不是说坚持这是一个坏原则。但我相信这种药物至少与症状一样糟糕-如果他们忘记正确使用药物(使用错误的方法添加听众的注意力),它们的状况将比正常解决方案更糟。
Kirk Woll

11

我已经看到大量的代码,其中未正确注销侦听器。这意味着它们仍然被不必要地调用来执行不必要的任务。

如果只有一个类依赖于侦听器,那么很容易清除,但是当25个类依赖于侦听器时会发生什么呢?正确注销它们变得更加棘手。事实是,您的代码可以从引用您的侦听器的一个对象开始,并在以后的版本中可以包含25个引用同一侦听器的对象。

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

WeakReferences充满了优点,因为它们会自动清理。唯一的缺点是您一定不要忘记在代码的其他地方保留硬引用。通常,这将依赖于该侦听器。

我讨厌创建监听器的匿名类实例的代码(正如Kirk Woll提到的那样),因为一旦注册,就无法再注销这些监听器。您没有对它们的引用。恕我直言,这真的很糟糕。

您也可以null在不再需要监听器时对其进行引用。您无需再为此担心。


您可以获取给定组件的侦听器数组。匿名类可以扩展专业化。给该专门的侦听器类一个uid属性,您就已经准备好有选择地删除匿名侦听器。
alphazero 2011年

1
您不喜欢的匿名侦听器只有在永远不要取消注册的情况下才应该注册。如果将其保存到变量/字段/集合中以供以后注销,那么您认为可以接受吗?
Bringer128 2011年

1
虽然我同意您的观点,匿名侦听器是一把双刃剑,但对于使用弱引用的我来说也是如此:将对侦听器的引用为空以将其删除会有些反语义。您需要在这一行中添加注释,这意味着以后任何时候都将取消注册侦听器。我认为,程序员负责注册侦听器,因此他也负责删除它们。
Manuel Leuenberger

@maenu我完全同意,注册和注销监听器是程序员的责任。没有讨论。我试图争辩的是,它是更简单,风险较小使用弱引用...
杰罗姆Verstrynge

这是依赖弱引用的另一个缺点-假设我在一个不断发展的模拟(模型)上有1000个侦听器(视图),并且视图可以非常动态地来去去去。假设某些用户操作(例如,交互式窗口调整大小)可能导致所有视图消失并被重新创建,例如100次。然后,我有100,000个失效的侦听器...如果观察者将其保留为WR,则它们最终将由GC清理...但是直到那时候,他们都将倾听并做可能昂贵的工作,从而导致(暂时)显着降低。
唐·哈奇

5

确实没有专业人士。弱引用通常用于“可选”数据,例如您不想阻止垃圾收集的缓存。您不希望收集您的监听器垃圾,而是希望它继续监听。

更新:

好的,我想我可能已经知道您正在做什么。如果将短期侦听器添加到长期生存的对象,则可能会受益于使用weakReference。因此,例如,如果您要向域对象添加PropertyChangeListeners以更新不断重新创建的GUI的状态,则域对象将保留在可能会建立的GUI上。考虑一个不断被重新创建的大型弹出对话框,其中一个侦听器引用通过PropertyChangeListener返回到Employee对象。如果我错了,请纠正我,但我认为整个PropertyChangeListener模式不再受欢迎。

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

这是一些有趣的读物:

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

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


1
“可选”数据(例如缓存)的正确引用类型是SoftReference,而不是WeakReference。WeakReference通常用于防止引用泄漏,与SoftReferences相比,显式地获得更高的最终确定优先级,这样它们就不会在GC扫描期间干扰缓存项的存储。
user515655 '16

5

老实说,我并没有真正购买该想法,也没有真正希望您使用addWeakListener进行购买。也许只有我一个人,但这似乎是一个错误的好主意。起初它很诱人,但它可能暗示的问题却可以忽略不计。

使用weakReference,您不能确定在不再引用侦听器本身时是否将不再调用该侦听器。垃圾收集器可以在几毫秒后释放内存,或者永远不会释放。这意味着它可能会继续消耗CPU,并使诸如抛出异常之类的异常变得奇怪,因为不应调用侦听器。

swing的一个示例是尝试做只有在UI组件实际上已附加到活动窗口的情况下才能做的事情。这可能会引发异常,并影响通知程序使其崩溃并阻止对有效的侦听器进行非官方通知。

如前所述,第二个问题是匿名侦听器,它们可能过早被释放,甚至根本没有被通知或只有几次。

您试图实现的目标很危险,因为当您停止接收通知时,您将无法控制。它们可能会永远持续下去或停止得太早。


“您试图实现的目标很危险,因为当您停止接收通知时,您将无法控制。” - 这个。WeakReferences允许自动删除专门编写的侦听器,但仅当GC令人感觉良好时才可以删除它,这种情况永远不会发生,而且经常发生。
user515655 '16

我希望人们会忽略该论点的“或过早停止”部分,我认为这是一种干扰,因为那只是编程错误,需要谨慎避免,但这不是根本问题,当您提及时会在视觉上扫视您的答案时使人们倾向于忽略您的全部论点。您应该正确指出,“可能无限期持续”部分是真正的问题。
唐·哈奇

停止在不同情况下过早出现,但也有问题。如果您只是希望在应用程序的生命周期内保留某个侦听器,或者希望与创建要监听的事件的组件处于同一生命周期,并且您的侦听器在创建之后就被垃圾回收,那么这也是一个大问题。
Nicolas Bousquet

3

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

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


3

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

在实践中,我看到了很多软件可以永久保留收听者的声音。通常,程序员甚至都不知道应该注销它们。

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

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

此代码将注册侦听器,返回一个封装侦听器并具有方法的对象,keepFor会将侦听器添加到以实例参数为键的静态weakHashMap中。这样可以确保至少在不对someInstance和otherInstance进行垃圾回收的情况下注册侦听器。

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

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

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

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


如果垃圾回收发生在调用之后on()但之前keepFor()呢?大概那时可能已经返回的keepFor()呼叫weakReference.get()null吗?
glebm

@glebm,是的,您是对的,这是一个问题。可能性很小,但这会使情况更糟。默认情况下永久保留它,直到调用另一种方法可以解决该问题。或者只是使从返回对象到侦听器的引用不弱。因此,只要使用了返回的对象,就不会收集垃圾。
基督徒

2

我无法想到将WeakReferences用于侦听器的任何合法用例,除非您的用例以某种方式涉及在下一个GC周期之后明确不应该存在的侦听器(该用例当然是特定于VM /平台的)。

可以为SoftReferences设想一个稍微合法的用例,在该用例中,侦听器是可选的,但会占用大量堆,并且在可用堆大小开始变小时应首先使用。我想,某种可选的缓存或其他类型的辅助侦听器可能是候选方法。即使这样,您似乎还是希望监听器的内部组件利用SoftReferences,而不是监听器和被监听者之间的链接。

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

这是学术问题,还是您要解决的实际情况?如果是实际情况,我很想听听它是什么,并且您可能会获得更多,更少抽象的解决方法的建议。


怎么样:当某件事发生时,对象引发一个事件。您想要一个带有字段或方法的对象,该对象报告发生了多少次。如果在增加该字段的事件之外不存在对该对象的引用,则该对象应停止存在。如果构造并丢弃了许多监视寿命长的对象的对象,那么除非事件被弱订阅,否则这些对象将继续无限制地累积。
2013年

2

我对原始海报有3条建议。很抱歉,您需要重新使用旧线程,但是我认为我的解决方案以前并未在该线程中讨论。

首先,考虑遵循JavaFX库中的javafx.beans.values.WeakChangeListener示例。

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

轻松修改了“触发事件”方法,以取消引用XxxWeakListener并在WeakReference.get()返回null时将其删除。

remove方法现在有点麻烦,因为我需要遍历整个列表,这意味着我需要进行同步。

第三,在实施此策略之前,我采用了一种可能会有用的不同方法。(硬引用)侦听器收到一个新事件,他们对是否仍在使用进行了现实检查。如果不是,则它们从观察者中退订,从而可以对其进行GC。对于寿命短的侦听器,订阅了寿命长的Observables,检测过时很容易。

为了尊重那些规定“总是取消订阅侦听器的良好编程习惯,每当侦听器诉诸取消订阅本身时,我都确保创建一个日志条目并稍后在我的代码中更正此问题。


这是一个已有3年历史的问题,并且您的“答案”似乎还不是一个问题。
Jan Chrbolka

1
我确实为复活一个3岁的老员工道歉。但是,当我阅读此线程时,这对我来说是新手,我敢肯定,如果其他人使用Google“弱听者”功能,那么他们也会第一次偶然发现这个线程。我认为这些人将从我对整个讨论的贡献中受益。特别是,我举了一个具体的示例,其中在现代Java API(已提起)中使用了弱侦听器。我还建议使用“自我删除需要日志条目”策略来解决减轻使用弱听者的“风险”的方法。
Teto

1

如果您特别希望GC控制侦听器的生存期,则WeakListeners很有用。

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

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

在这里,弱监听器使子节点可以侦听父节点,同时其寿命也由可达性/缓存决定,因此该结构不会在内存中保留所有隐含节点。


0

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

我似乎还记得我们PropertyChangeSupport在ListView的行视图内使用的自定义侦听器之一存在一些问题。我们找不到注销这些监听器的好方法和可靠的方法,因此使用WeakReference监听器似乎是最干净的解决方案。


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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.