Java LinkedHashMap с removeEldestEntry вызывает java.lang.NullPointerException

Ошибка выглядит так

Exception in thread "Thread-1" java.lang.NullPointerException
    at java.util.LinkedHashMap$Entry.remove(LinkedHashMap.java:332)
    at java.util.LinkedHashMap$Entry.recordAccess(LinkedHashMap.java:356)
    at java.util.LinkedHashMap.get(LinkedHashMap.java:304)
    at Server.getLastFinishedCommands(Server.java:9086)
    at Server.processPacket(Server.java:484)
    at PacketWorker.run(PacketWorker.java:34)
    at java.lang.Thread.run(Thread.java:744)

внутри getLastFinishedCommands я использую

   public List<CCommand> getLastFinishedCommands(UserProfile player) {
        List<CCommand> returnList = new ArrayList<CCommand>();

        if(!finishedCommands.containsKey(player.myWebsitecmd-1)) {
            getSavedState(player);
            return null;
        }

        try { //<-- added this try/catch so it doesn't happen again.
            //Get commands.
            CCommand cmd;
            long i;
            long startIndex = player.myWebsitecmd;
            long endIndex = startIndex+LIMIT_COMMANDS;

            for(i = startIndex; i <= endIndex; i++) {
                cmd = finishedCommands.get(i);   //<-- this is line 9086
                if(cmd == null) {
                    return returnList;
                }
                returnList.add(cmd);
            }
        } catch(Exception e) {} //<-- added this try/catch so it doesn't happen again.
        return returnList;
    }

Я хотел создать карту, которая автоматически удаляет старые записи, поэтому я использовал этот фрагмент

public static <K, V> Map<K, V> createLRUMap(final int maxEntries) {
    return new LinkedHashMap<K, V>(maxEntries*3/2, 0.7f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > maxEntries;
        }
    };
}

Использовал это так

public static int final MAX_COMMANDS_QUEUE = 5000;
public Map<Long, CCommand> finishedCommands = createLRUMap(MAX_COMMANDS_QUEUE);

Очевидно, что это какое-то CocurrentModifcationException, которое происходит при использовании с несколькими потоками... но почему это происходит сбой внутри, кто-нибудь знает, как я могу использовать это как CocurrentHashMap? Я пытаюсь это исправить, не прибегая к тому, чтобы просто попытаться поймать getLastFinishedCommands функция.

Мне нужна карта, которая очищается от старого барахла, но все еще содержит не менее 5000 записей ключ / значение.

2 ответа

Решение

Вы сказали, что несколько потоков обращаются к этой карте. Это действительно может вызвать NPE в remove операция LinkedHashMap.Entry пример. Это реализация этого метода:

private void remove() {
    before.after = after;
    after.before = before;
}

Вот before и после refer связанному предшественнику и преемнику текущей записи. Если другой поток уже изменил связь между записями, это, конечно, может привести к неожиданному поведению, например, NPE.

Решение состоит в том, чтобы - как вы уже догадались - обернуть созданную вами карту в синхронизированную карту. Такие как:

public static <K, V> Map<K, V> createLRUMap(final int maxEntries) {
    Map<K,V> result = new LinkedHashMap<K, V>(maxEntries*3/2, 0.7f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > maxEntries;
        }
    };
    return Collections.synchronizedMap(result);
}

Эта синхронизированная оболочка действительно синхронизирует все вызовы с базовой картой, поэтому только один единственный поток может проходить через каждый метод (например, получить, поместить, содержит, размер и т. Д.).

Основываясь на трассировке стека, я предполагаю, что код пытается удалить значение из индекса, элемент которого уже был удален другим потоком. Это делает это, чтобы бросить NPE при доступе к свойствам null ссылка. Возможно, стоит попробовать синхронизировать коллекцию

Из документации LinkedHashMap

Обратите внимание, что эта реализация не синхронизирована. Если несколько потоков обращаются к связанной хэш-карте одновременно, и хотя бы один из потоков структурно изменяет карту, она должна быть синхронизирована извне. Обычно это достигается путем синхронизации с некоторым объектом, который естественным образом инкапсулирует карту. Если такого объекта не существует, карту следует "обернуть" с помощью метода Collections.synchronizedMap. Это лучше всего делать во время создания, чтобы предотвратить случайный несинхронизированный доступ к карте:

   Map m = Collections.synchronizedMap(new LinkedHashMap(...));
Другие вопросы по тегам