迭代期间插入HashMap

时间:2014-08-09 14:42:35

标签: java multithreading events collections

我在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();

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

3 个答案:

答案 0 :(得分:1)

我还没有完全分析您的问题,但正常的方法是使用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。即使在期间也不需要同步   迭代,并保证迭代器永远不会抛出   ConcurrentModificationException的。这种实现非常适合   维护事件处理程序列表,其中更改很少发生,并且   遍历频繁且可能耗费时间。

答案 1 :(得分:1)

如果您的解决方案允许您实施Thread某些内容可能是错误的。

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

类似于:

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

因此,如果您的handle事件将添加新事件,则副本将无法查看将遍历所有原始事件的副本。

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

答案 2 :(得分:1)

  

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

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

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

无论如何,根据此Q&amp; A - Java thread creation overhead - 创建和启动一个主题需要0.1毫秒的时间。


我可以想到两种方法:

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

  • 将听众直接放入eventListeners地图,但要实施一些措施以防止他们提前解雇。例如,您与事件序列号进行比较的每个侦听器标志或整数值激活字段。

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