Почему замена EventListenerList не была заменена? (Или: какие подводные камни заменяют его?) - программирование
Подтвердить что ты не робот

Почему замена EventListenerList не была заменена? (Или: какие подводные камни заменяют его?)

Наше устаревшее приложение застряло в ужасной структуре (хорошо, я назову имена, это Tapestry 4), что связано с нелепым числом EventListeners (~ 100 000) для самых простых операций. Я предполагаю, что это выходит за рамки того, что javax.swing.event.EventListenerList когда-либо предназначалось для обработки, и в этом неудачном случае использования это вызывает у нас некоторые неприятные головные боли производительности.

Я провел пару часов, подбирая довольно наивную замену на основе HashMap/ArrayList ниже, и она значительно быстрее почти всеми способами:

Добавить 50 000 слушателей:

  • EventListenerList > 2 секунды
  • EventListenerMap ~ 3,5 миллисекунды

Событие пожара до 50 000 слушателей:

  • EventListenerList 0,3-0,5 миллисекунды
  • EventListenerMap 0,4-0,5 миллисекунды

Удалите 50 000 слушателей (по одному):

  • EventListenerList > 2 секунды
  • EventListenerMap ~ 280 миллисекунд

Увольнение может быть только медленнее, но модификация значительно быстрее. По общему признанию, ситуация, с которой эта структура вложила нас в себя, является патологическим, но по-прежнему кажется, что EventListenerList можно было заменить давно. Очевидно, что есть проблемы с публичным API (например, он раскрывает свой внутренний внутренний массив состояний), но для этого должно быть больше. Может быть, есть многопоточные случаи, когда EventListenerList намного безопаснее или более результативен?

public class EventListenerMap
{

    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    private Map<Class, List> llMap = new HashMap<Class, List>();

    public <L extends EventListener> void add ( Class<L> listenerClass, L listener )
    {
        try
        {
            writeLock.lock();
            List<L> list = getListenerList( listenerClass );
            if ( list == null )
            {
                list = new ArrayList<L>();
                llMap.put( listenerClass, list );
            }
            list.add( listener );
        }
        finally
        {
            writeLock.unlock();
        }
    }

    public <L extends EventListener> void remove ( Class<L> listenerClass, L listener )
    {
        try
        {
            writeLock.lock();
            List<L> list = getListenerList( listenerClass );
            if ( list != null )
            {
                list.remove( listener );
            }
        }
        finally
        {
            writeLock.unlock();
        }
    }

    @SuppressWarnings("unchecked")
    public <L extends EventListener> L[] getListeners ( Class<L> listenerClass )
    {
        L[] copy = (L[]) Array.newInstance( listenerClass, 0 );
        try
        {
            readLock.lock();
            List<L> list = getListenerList( listenerClass );
            if ( list != null )
            {
                copy = (L[]) list.toArray( copy );
            }
        }
        finally
        {
            readLock.unlock();
        }
        return copy;
    }

    @SuppressWarnings("unchecked")
    private <L extends EventListener> List<L> getListenerList ( Class<L> listenerClass )
    {
        return (List<L>) llMap.get( listenerClass );
    }
}
4b9b3361

Ответ 1

Это вопрос оптимизации. Swing EventListenerList предполагает:

  • Количество слушателей в списке очень мало.
  • Количество ListenerLists может быть очень большим.
  • Добавить/удалить события очень редки

Учитывая эти предположения, вычислительные затраты на добавление и удаление элементов незначительны, но стоимость памяти при наличии этих списков может быть значительной. Поэтому EventListenerList работает, выделяя массив, который достаточно велик, чтобы удерживать Listeners, таким образом, имея минимально возможный объем памяти. (Как отмечает docs, он даже не выделяет ничего до тех пор, пока не будет добавлен первый прослушиватель, чтобы убедиться, что вы не теряете места, если нет прослушивателей.) Недостатком этого является то, что каждый раз, когда добавляется новый элемент, он переназначает массив и копирует все старые элементы, предоставляя вам астрономические затраты, когда у вас слишком много слушателей.

(На самом деле, он не настолько эффективен по мере возможности памяти, список - это пары {Type, Listener}, поэтому, если бы это был массив массивов, в некоторых случаях он был бы немного меньше.)

Что касается вашего решения: HashMap перераспределить память, чтобы обеспечить эффективное хеширование. Аналогично, конструктор ArrayList по умолчанию выделяет пространство для 10 элементов и растет в кусках. В вашей причудливой кодовой базе, где в каждом списке есть 100k слушателей, эта дополнительная память является крошечным дополнением к памяти, которую вы используете, чтобы удерживать всех слушателей.

Однако при загрузке более легкого прослушивателя ваша реализация стоит 16 указателей для каждого пустого списка (распределение по умолчанию HashMap), 26 указателей для EventListenerMap с одним элементом и 36 указателей для карты с двумя элементами разных классов. (Это не означает остальную часть размера структуры HashMap и ArrayList.) Для тех же случаев EventListenerList стоит 0, 2 и 4 указателя соответственно.

Похоже, что это значительно улучшает код, который у вас есть.