Является ли HashMap потокобезопасным для разных ключей?

87

Если у меня есть два нескольких потока, обращающихся к HashMap, но гарантирую, что они никогда не будут обращаться к одному и тому же ключу одновременно, может ли это привести к состоянию гонки?

Helder S Ribeiro
источник

Ответы:

99

В ответе @dotsid он говорит следующее:

Если вы каким-либо образом измените HashMap, ваш код просто сломается.

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

  • Если один поток выполняет a put, то другой поток может увидеть устаревшее значение размера хэш-карты.

  • Когда поток выполняет putоперацию, которая запускает перестройку таблицы, другой поток может видеть временные или устаревшие версии ссылки на массив хэш-таблицы, ее размер, ее содержимое или цепочки хеш-кодов. Может возникнуть хаос.

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

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

И если у вас есть два потока, одновременно выполняющих putили removeзапрашивающих, существует множество возможностей для состояний гонки.

Я могу придумать три решения:

  1. Используйте файл ConcurrentHashMap.
  2. Используйте обычный, HashMapно синхронизируйте снаружи; например, с использованием примитивных мьютексов, Lockобъектов и т. д.
  3. Используйте разные HashMapдля каждого потока. Если потоки действительно имеют непересекающийся набор ключей, тогда не должно быть необходимости (с алгоритмической точки зрения) для них совместно использовать одну карту. В самом деле, если ваши алгоритмы включают потоки, повторяющие ключи, значения или записи карты в какой-то момент, разделение одной карты на несколько карт может дать значительное ускорение для этой части обработки.
Стивен С
источник
30

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

Чтобы ответить на ваш исходный вопрос: согласно javadoc, пока структура карты не меняется, все в порядке. Это означает отсутствие удаления элементов и добавления новых ключей, которых еще нет на карте. Заменить значение, связанное с существующими ключами, можно.

Если несколько потоков обращаются к хеш-карте одновременно, и хотя бы один из потоков структурно модифицирует карту, она должна быть синхронизирована извне. (Структурная модификация - это любая операция, которая добавляет или удаляет одно или несколько сопоставлений; простое изменение значения, связанного с ключом, который уже содержит экземпляр, не является структурной модификацией.)

Хотя это не дает никаких гарантий относительно видимости. Поэтому вы должны быть готовы время от времени получать устаревшие ассоциации.

Тим Бендер
источник
6

Это зависит от того, что вы имеете в виду под «доступом». Если вы просто читаете, вы можете читать даже те же ключи, если видимость данных гарантируется в соответствии с правилами « происходит раньше ». Это означает, что это HashMapне должно меняться, и все изменения (начальные конструкции) должны быть завершены до того, как любой читатель начнет доступ HashMap.

Если вы HashMapкаким-либо образом измените a, ваш код просто сломается. @Stephen C дает очень хорошее объяснение, почему.

РЕДАКТИРОВАТЬ: Если первый случай - это ваша реальная ситуация, я рекомендую вам использовать, Collections.unmodifiableMap()чтобы быть уверенным, что ваш HashMap никогда не изменяется. Объекты, на которые указывает, также HashMapне должны изменяться, поэтому агрессивное использование finalключевого слова может вам помочь.

И, как говорит @Lars Andren, ConcurrentHashMapв большинстве случаев это лучший выбор.

Денис Баженов
источник
2
На мой взгляд, ConcurrentHashMap - лучший выбор. Единственная причина, по которой я не рекомендовал это, потому что автор не спрашивал об этом :) У него меньше пропускная способность из-за операций CAS, но, как гласит золотое правило параллельного программирования: «Сделайте это правильно, и только потом сделайте это быстро. ":)
Денис Баженов
unmodifiableMapгарантирует, что клиент не сможет изменить карту. Он ничего не делает, чтобы гарантировать, что основная карта не изменится.
Пит Киркхэм,
Как я уже отмечал: «Объекты, на которые указывает HashMap, также не должны меняться»
Денис Баженов
4

Изменение HashMap без надлежащей синхронизации из двух потоков может легко привести к состоянию гонки.

  • Когда a put()приводит к изменению размера внутренней таблицы, это занимает некоторое время, и другой поток продолжает запись в старую таблицу.
  • Два put()для разных ключей приводят к обновлению одного и того же блока, если хэш-коды ключей равны по модулю размера таблицы. (На самом деле связь между хэш-кодом и индексом корзины более сложная, но коллизии все же могут возникать.)
Кристиан Семрау
источник
1
Это хуже, чем просто гоночные условия. В зависимости от внутреннего устройства HashMapреализации, которое вы используете, вы можете получить повреждение HashMapструктур данных и т.д., вызванное аномалиями памяти.
Стивен С.