Если у меня есть два потока, обращающихся к HashMap, но гарантируйте, что они никогда не будут обращаться к одному и тому же ключу одновременно, может ли это привести к состоянию гонки?
Является ли HashMap потокобезопасным для разных ключей?
Ответ 1
В ответе @dotsid он говорит следующее:
Если вы меняете HashMap каким-либо образом, ваш код просто сломан.
Он прав. HashMap, обновленный без синхронизации, сломает даже, если потоки используют непересекающиеся наборы ключей. Вот некоторые из вещей, которые могут пойти не так.
-
Если один поток выполняет
put
, то другой поток может видеть устаревшее значение для размера hashmap. -
Когда поток выполняет
put
, который инициирует перестроение таблицы, другой поток может видеть переходные или устаревшие версии справки массива хэш-таблицы, его размер, его содержимое или хэш-цепочки. Может наступить хаос. -
Когда поток выполняет
put
для ключа, который сталкивается с некоторым ключом, используемым каким-то другим потоком, и последний поток выполняетput
для своего ключа, тогда последний может видеть устаревшую копию ссылка на хеш-цепь. Может наступить хаос. -
Когда один поток проверяет таблицу с ключом, который сталкивается с одним из некоторых других ключей потока, он может столкнуться с этим ключом в цепочке. Он будет вызывать equals на этом ключе, и если потоки не синхронизированы, метод equals может столкнуться с устаревшим состоянием в этом ключе.
И если у вас есть два потока, одновременно выполняющих запросы put
или remove
, существует множество возможностей для условий гонки.
Я могу представить три решения:
- Используйте
ConcurrentHashMap
. - Используйте обычный
HashMap
, но синхронизируйте снаружи; например используя примитивные мьютексы, объектыLock
и т.д. - Используйте разные
HashMap
для каждого потока. Если в потоках действительно есть непересекающийся набор ключей, то для них не должно быть необходимости (с точки зрения алгоритма) делиться одной Картой. В самом деле, если ваши алгоритмы включают в себя потоки, повторяющие ключи, значения или записи карты в какой-то момент, разделение одной карты на несколько карт может дать значительное ускорение для этой части обработки.
Ответ 2
Просто используйте ConcurrentHashMap. ConcurrentHashMap использует несколько блокировок, которые охватывают целый ряд хэш-кодов, чтобы уменьшить вероятность блокировки. Существует незначительное влияние на производительность незащищенной блокировки.
Чтобы ответить на ваш первоначальный вопрос: согласно javadoc, до тех пор, пока структура карты не изменится, вы в порядке. Это означает, что вообще не удалять элементы и не добавлять новые ключи, которые еще не находятся на карте. Замена значения, связанного с существующими ключами, прекрасна.
Если несколько потоков обращаются к карте хешей одновременно, и по крайней мере один из потоков изменяет структуру структурно, она должна быть синхронизирована извне. (Структурная модификация - это любая операция, которая добавляет или удаляет одно или несколько сопоставлений, просто изменяя значение, связанное с ключом, который уже содержит экземпляр, не является структурной модификацией.)
Хотя он не дает никаких гарантий относительно видимости. Таким образом, вы должны быть готовы согласиться на получение устаревших ассоциаций.
Ответ 3
Это зависит от того, что вы подразумеваете под "доступом". Если вы просто читаете, вы можете читать даже те же клавиши, пока видимость данных, гарантированных в соответствии с правилами before-before". Это означает, что HashMap
не должен меняться, и все изменения (начальные конструкции) должны быть завершены до того, как любой читатель начнет получать доступ к HashMap
.
Если вы измените HashMap
каким-либо образом, ваш код просто сломается. @Stephen C дает очень хорошее объяснение, почему.
РЕДАКТ. Если первый случай - это ваша реальная ситуация, я рекомендую вам использовать Collections.unmodifiableMap()
, чтобы быть уверенным, что ваш HashMap никогда не изменяется. Объекты, на которые указывает HashMap
, также не должны меняться, поэтому агрессивность с использованием ключевого слова final
может помочь вам.
И как @Lars Andren говорит, ConcurrentHashMap
- лучший выбор в большинстве случаев.
Ответ 4
Изменение HashMap без надлежащей синхронизации из двух потоков может легко привести к состоянию гонки.
- Когда a
put()
приводит к изменению размера внутренней таблицы, это занимает некоторое время, а другой поток продолжает записываться в старую таблицу. - Два
put()
для разных клавиш приводят к обновлению одного и того же ведра, если хэш-коды ключей равны по модулю размера таблицы. (На самом деле связь между индексом hashcode и bucket более сложна, но столкновения могут все еще происходить.)