【问题标题】:HashMap insert during iterationHashMap 在迭代期间插入
【发布时间】:2014-10-02 21:20:06
【问题描述】:

我在 Map 中维护可变数量的事件侦听器:

private final Map<String, EventListener> eventListeners = new ConcurrentHashMap<>();

使用地图的类有添加事件监听的方法:

public void addEventListener(final String name, final EventListener listener) {
    eventListeners.put(name, listener);
}

每次发生事件时,我都会遍历所有侦听器并触发它们:

eventListeners.forEach((name, listener) -> {
    listener.handle(event);
});

环境是单线程的,有趣的部分是事件监听器实现可能会在它们被触发时插入另一个事件监听器。

使用“普通”HashMap,这显然会导致ConcurrentModificationException,这就是我使用ConcurrentHashMap的原因。

我看到的情况是,由另一个事件侦听器插入的事件侦听器可能会被同一个事件触发,因为它是在迭代期间插入的,我认为这是ConcurrentHashMap 的预期行为。

但是,我不希望这种情况发生,所以我想推迟事件侦听器的任何插入,直到对所有侦听器的迭代完成。

所以我引入了一个Thread,它在使用CountDownLatch 插入侦听器之前等待迭代完成:

public void addEventListener(final String name, final EventListener listener) {
    new Thread(() -> {
        try {
            latch.await();
        } catch (InterruptedException e) {}
        eventListeners.put(name, listener);
    }).start();
}

以及对所有侦听器的迭代:

latch = new CountDownLatch(1);
eventListeners.forEach((name, listener) -> {
    listener.handle(event);
});
latch.countDown();

它按预期工作,但我想知道这是否是一个好的解决方案,是否有更优雅的方法来实现我需要的。

【问题讨论】:

    标签: java multithreading events collections


    【解决方案1】:

    如果您的解决方案允许您实施 Thread,则可能有问题。

    您可以避免迭代原始地图并迭代它的副本。

    比如:

    Map<String, EventListener> eventListenerCopy = new HashMap<>(eventListeners);
    eventListenerCopy.forEach((name, listener) -> {
      listener.handle(event);
    });
    

    因此,如果您的 handle 事件将添加新事件,它将不会被复制所有原始事件的副本看到。

    或者,如果您强制使用特定方法添加新侦听器,您可以创建一个临时列表,其中将保存所有新侦听器,并在迭代完成时检查此新列表,如果存在则将新侦听器放入原始列表中(如果您不想每次都创建地图副本,这可能会更好)。

    【讨论】:

    • 我的问题和你的回答是一个很好的例子,因为可能总是一个简单的解决方案比一个复杂的解决方案好得多。
    【解决方案2】:

    我还没有完全分析您的问题,但通常的方法是使用CopyOnWriteArraYList

    public class CopyOnWrite {
       private CopyOnWriteArrayList<ActionListener> list = 
               new CopyOnWriteArrayList<>();
    
       public void fireListeners() {
          for( ActionListener el : list ) 
             el.actionPerformed( new ActionEvent( this, 0, "Hi" ) );
       }
    
       public void addListener( ActionListener al ) {
          list.add( al );
       }
    }
    

    CopyOnWriteArrayList 是线程安全的,也不会抛出 ConcurrentModificationException

    Link

    CopyOnWriteArrayList 是一个由 a 支持的 List 实现 写时复制数组。这个实现本质上类似于 CopyOnWriteArraySet。无需同步,即使在 迭代,并且保证迭代器永远不会抛出 并发修改异常。这个实现非常适合 维护事件处理程序列表,其中不经常更改,以及 遍历很频繁,而且可能很耗时。

    【讨论】:

    • 我认为使用 CopyOnWriteArrayList 是一个不错的建议,但在这种情况下我需要一张地图。
    【解决方案3】:

    它按预期工作,但我想知道这是否是一个好的解决方案,是否有更优雅的方法来实现我需要的。

    它很优雅(我猜),但它不是一个好的解决方案。如果我理解你在这里做什么,那么每次你想添加一个监听器时都会产生一个线程。这是非常昂贵的。

    在典型的 JVM 中,线程有一个很大的堆外内存段来保存线程堆栈。每次启动线程时,都会进行系统调用来分配内存,并进行其他系统调用来创建和启动本机线程。

    无论如何,根据这个问答 - Java thread creation overhead - 创建和启动一个线程需要大约 0.1 毫秒。


    我可以想到两种方法来做到这一点:

    • 不要在迭代期间将新侦听器添加到 eventListeners,而是将它们添加到临时列表,然后在您完成触发事件后将其附加到主 eventListeners 映射。

    • 将侦听器直接放入eventListeners 映射中,但要实施一些措施以防止它们过早触发。例如,与事件序列号进行比较的每个侦听器标志或整数值激活字段。

    (其他人建议使用CopyOnWriteList,这是一个很好的建议,除非eventListener 列表/地图可能很大和/或可能会频繁添加听众。)

    【讨论】:

    • 我接受了 Marco Acierno 的建议,该建议与您的建议类似,但您对性能影响提出了非常好的观点。
    猜你喜欢
    • 2011-05-13
    • 1970-01-01
    • 2018-09-16
    • 1970-01-01
    • 2011-03-16
    • 2015-11-25
    • 1970-01-01
    • 2013-03-20
    相关资源
    最近更新 更多