Является ли 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 более сложна, но столкновения могут все еще происходить.)