Поведение entrySet(). RemoveIf в ConcurrentHashMap
Я хотел бы использовать ConcurrentHashMap, чтобы один поток удалял некоторые элементы с карты периодически и другие потоки, чтобы одновременно помещать и получать элементы с карты.
Я использую map.entrySet().removeIf(lambda)
в удаляемой теме. Мне интересно, какие предположения я могу сделать о его поведении. Я вижу, что метод removeIf
использует итератор для прохождения элементов на карте, проверки данного условия и последующего удаления при необходимости с помощью iterator.remove()
.
Документация дает некоторую информацию о поведении итераторов ConcurrentHashMap:
Аналогично, итераторы, разделители и перечисления возвращают элементы отражающие состояние хеш-таблицы в какой-то момент времени или после создание итератора/перечисления. эй не бросать ConcurrentModificationException. Однако итераторы предназначены для использования только по одному потоку за раз.
Поскольку весь вызов removeIf
происходит в одном потоке, я могу быть уверен, что итератор не используется более чем одним потоком в то время. Тем не менее мне интересно, возможен ли описанный ниже ход событий:
- Карта содержит отображение:
'A'->0
- Удаление темы начинается с
map.entrySet().removeIf(entry->entry.getValue()==0)
- Удаление вызовов потоков
.iteratator()
внутри removeIf
вызывает и получает итератор, отражающий текущее состояние коллекции
- Другой поток выполняет
map.put('A', 1)
- Удаление потока по-прежнему видит
'A'->0
mapping (итератор отражает старое состояние), а поскольку 0==0
- true, он решает удалить ключ A с карты.
- Теперь карта содержит
'A'->1
, но удаление потока увидело старое значение 0
и запись 'A' ->1
удалена, хотя ее не должно быть. Карта пуста.
Я могу представить, что поведение может быть предотвращено реализацией по-разному. Например: возможно, итераторы не отражают операции put/remove, но всегда отражают обновления значений или, возможно, метод удаления итератора проверяет, все ли отображение (как ключ, так и значение) все еще присутствует на карте, прежде чем вызывать удаление ключа. Я не мог найти информацию об этих вещах, и мне интересно, есть ли что-то, что делает этот сейф безопасным.
Ответы
Ответ 1
Мне также удалось воспроизвести такой случай на моей машине.
Я думаю, проблема в том, что EntrySetView
(который возвращается ConcurrentHashMap.entrySet()
) наследует реализацию removeIf
от Collection
, и выглядит так:
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
// `test` returns `true` for some entry
if (filter.test(each.next())) {
// entry has been just changed, `test` would return `false` now
each.remove(); // ...but we still remove
removed = true;
}
}
return removed;
}
По моему скромному мнению, это не может рассматриваться как правильная реализация для ConcurrentHashMap
.
Ответ 2
После обсуждения с пользователем Zielu в комментариях ниже ответа Zielu я углубился в код ConcurrentHashMap и узнал, что:
- ConcurrentHashMap реализует метод
remove(key, value)
, который вызывает replaceNode(key, null, value)
-
replaceNode
проверяет, остаются ли все ключи и значение на карте перед удалением, поэтому использование должно быть прекрасным. Документация гласит, что она
Заменяет значение node значением v, обусловленным совпадением cv if * не null.
- В случае, указанном в вопросе ConcurrentHashMap
.entrySet()
, вызывается, который возвращает класс EntrySetView
. Затем метод removeIf
вызывает .iterator()
, который возвращает EntryIterator
.
-
EntryIterator
extends BaseIterator
и наследует реализацию remove
, которая вызывает map.replaceNode(p.key, null, null)
, которая отключает условное удаление и всегда удаляет ключ.
Отрицательный ход событий можно было бы предотвратить, если итераторы всегда повторяли "текущие" значения и никогда не возвращали старые, если какое-то значение было изменено. Я все еще не знаю, случается это или нет, но приведенный ниже тестовый пример, похоже, проверяет все это.
Я думаю, что создал тестовый пример, который показывает, что поведение, описанное в моем вопросе, действительно может произойти. Пожалуйста, исправьте меня, если я ошибаюсь в коде.
Код запускает два потока. Один из них (DELETING_THREAD) удаляет все записи, сопоставленные с логическим значением "false". Другой (ADDING_THREAD) случайным образом помещает значения (1, true)
или (1,false)
в карту. Если он помещает true
в значение, он ожидает, что запись все равно будет там, когда будет отмечена, и выбрасывает исключение, если это не так. Он быстро генерирует исключение, когда я запускаю его локально.
package test;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
public class MainClass {
private static final Random RANDOM = new Random();
private static final ConcurrentHashMap<Integer, Boolean> MAP = new ConcurrentHashMap<Integer, Boolean>();
private static final Integer KEY = 1;
private static final Thread DELETING_THREAD = new Thread() {
@Override
public void run() {
while (true) {
MAP.entrySet().removeIf(entry -> entry.getValue() == false);
}
}
};
private static final Thread ADDING_THREAD = new Thread() {
@Override
public void run() {
while (true) {
boolean val = RANDOM.nextBoolean();
MAP.put(KEY, val);
if (val == true && !MAP.containsKey(KEY)) {
throw new RuntimeException("TRUE value was removed");
}
}
}
};
public static void main(String[] args) throws InterruptedException {
DELETING_THREAD.setDaemon(true);
ADDING_THREAD.start();
DELETING_THREAD.start();
ADDING_THREAD.join();
}
}