Руководство по Java ConcurrentMap

ConcurrentMap — это интерфейс Java Collections Framework, который используется для создания потокобезопасной карты в Java. Он сохраняет объекты в виде пар ключ-значение в карте, но синхронизированным образом.

Хотя у нас уже есть HashMap и HashTable в Java, ни один из них не работает хорошо в контексте параллелизма. Поэтому рекомендуется использовать параллельную карту в потокобезопасном приложении.

1. Как работает Java ConcurrentMap?

Внутри себя ConcurrentMap использует сегменты данных(осколки или разделы), разделяя карту внутри на это количество разделов(по умолчанию 16). При выполнении операций добавления или обновления потоком ConcurrentMap блокирует тот конкретный сегмент, где должно произойти обновление. Но он позволяет другим потокам считывать любое значение из других разблокированных сегментов.

Это означает, что нам не нужны синхронизированные блоки при доступе к ConcurrentMap в многопоточных приложениях, поскольку согласованность данных поддерживается внутренне.

Руководство по Java ConcurrentMap0

В обычных приложениях один шард хранит разумное количество пар ключ-значение и позволяет нескольким потокам выполнять операции чтения. И производительность чтения также очень оптимальна. Таблица динамически расширяется, когда коллизий слишком много.

Обратите внимание, что результаты методов size(), isEmpty() и containsValue() отражаютпереходное состояние карты и обычно полезны для мониторинга или оценки, но не для управления программой.

2. Реализации ConcurrentMap

Следующие классы реализуют ConcurrentMap на Java.

2.1. ConcurrentHashMap

ConcurrentHashMap — это класс реализации ConcurrentMap, аналогичный HashTable, за исключением того, что он хранит данные в небольших сегментах памяти, чтобы сделать их доступными для параллельных потоков независимо.

По умолчанию он создает 16 сегментов, к которым могут получить доступ параллельные потоки, и которые заблокированы для изменения записей. Он использует концепцию «происходит раньше» для обновления записей. Он не выполняет никаких блокировок для операций чтения и предоставляет потоку последние обновленные данные.

2.2. ConcurrentSkipListMap

Это класс реализации ConcurrentMap и ConcurrentNavigableMap. Он хранит данные либо в естественном порядке сортировки, либо в порядке, указанном Comparator во время его инициализации. Его реализация основана на структуре данных SkipLists, которая имеет общую сложность O(log n) для операций вставки, удаления и поиска.

Также обратите внимание, что ключи в ConcurrentHashMap не отсортированы, поэтому для случаев, когда требуется упорядочивание, ConcurrentSkipListMap — лучший выбор. Это параллельная версия TreeMap. По умолчанию она упорядочивает ключи в порядке возрастания.

3. Конкурентные операции Map

Давайте научимся выполнять различные операции на параллельных картах.

3.1 Создание ConcurrentMap

Чтобы использовать ConcurrentMap, мы можем использовать создание экземпляра любого из его классов реализации. Мы можем вызвать конструктор и передать требуемые аргументы, такие как начальная емкость, коэффициент загрузки и уровень параллелизма.

  • Конструктор по умолчанию создаст пустой ConcurrentMap с initialCapacity 16 и коэффициентом загрузки 0,75f.
  • LoadFactor управляет плотностью упаковки внутри карты, дополнительно оптимизируя использование памяти.
  • concurrencyLevel управляет количеством шардов в карте. Например, уровень параллелизма, установленный на 1, гарантирует, что будет создан и поддержан только один шард.

Обратите внимание, что эти параметры влияют только на начальный размер карты. Они не могут быть учтены при изменении размера карты.

ConcurrentMap<Integer, String> cmap = new ConcurrentHashMap<>();ConcurrentHashMap(int initialCapacity);ConcurrentHashMap(int initialCapacity, float loadFactor);ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel);

Передайте существующую карту ее конструктору для инициализации ConcurrentMap с теми же записями, что и у данной карты.

ConcurrentHashMap(Map<? extends K,? extends V> m)

3.2 Добавление записей

Чтобы добавить элементы в параллельную карту, мы можем использовать один из следующих методов:

  • put(key, value): принимает два аргумента, первый аргумент — ключ, а второй — значение. Ни ключ, ни значение не могут быть нулевыми.
  • putIfAbsent(key, value): если указанный ключ еще не связан со значением(или сопоставлен с null), связывает его с заданным значением и возвращает null, в противном случае возвращает текущее значение.
  • computeIfAbsent(key, mappingFunction): Если указанный ключ еще не связан со значением, он пытается вычислить значение с помощью заданной функции сопоставления и вводит его в карту, если только это не null. Этот метод очень полезен, когда вычисление значения является дорогостоящей операцией, например, получение значения из удаленной системы или базы данных. Этот метод гарантирует, что вычисление будет выполнено только тогда, когда значение отсутствует на карте, тем самым предотвращая ненужные вычисления.

Для операций compute… и merge…, если вычисленное значение равно null, то сопоставление ключ-значение удаляется, если оно присутствует, или остается отсутствующим, если оно ранее отсутствовало.

ConcurrentMap<Integer, String> cmap = new ConcurrentHashMap<>();cmap.put(1, "Delhi");cmap.putIfAbsent(2, "NewYork");cmap.computeIfAbsent("3", k -> getValueFromDatabase(k));

3.3 Удаление записей

Используйте метод remove() для удаления записи по ее ключу.

cmap.remove(2);

3.4. Итерация по записям

Для перебора ключей, значений или записей ConcurrentMap мы можем использовать простой цикл for или расширенный цикл for.

ConcurrentMap<Integer, String> cmap = new ConcurrentHashMap<>();cmap.put(1, "Delhi");cmap.put(2, "NewYork");cmap.put(3, "London");// Iterating concurrent map keysfor(Integer entry : cmap.keySet()) {System.out.println("Entry -- " + entry);}// Iterating concurrent map valuesfor(String value : cmap.values()) {System.out.println("Value -- " + value);}// Iterating concurrent map entriesfor(Map.Entry<Integer, String> entry : cmap.entrySet()) {System.out.println(entry.getKey() + " -- " + entry.getValue());}

ConcurrentMap также поддерживает потоковые операции. Во время массовых операций в Stream, аналогично итераторам выше, он не выдает ConcurrentModificationException.

Stream.of(cmap.entrySet()).forEach(System.out::println);

3.5 Преобразование HashMap в ConcurrentMap

Чтобы преобразовать HashMap в ConcurrentMap, используйте его конструктор и передайте хэш-карту в качестве аргумента конструктора.

Map<Integer, String> hashmap = ...;ConcurrentMap<Integer, String> cmap = new ConcurrentHashMap<>(hashmap);

4. Обработка отсутствующих ключей в ConcurrentMap

Java добавила новый метод getOrDefault() в свою версию 1.8 для обработки отсутствующих ключей. Этот метод возвращает значение по умолчанию, если указанный ключ не существует в ConcurrentMap.

ConcurrentMap<Integer, String> cmap = new ConcurrentHashMap<>();cmap.put(1, "Delhi");cmap.put(2, "NewYork");cmap.put(3, "London");String val = cmap.getOrDefault(1,"Bombay");System.out.println("Value = "+val); //Prints Delhival = cmap.getOrDefault(10, "Kolkata");System.out.println("Value = "+val); //Prints Kolkata

5. Заключение

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

Мы также научились выполнять различные операции на картах, используя примеры и передовой опыт для достижения оптимальной производительности.

Исходный код на Github

Прокрутить вверх