Является ли установка потокобезопасной HashMap?

У меня есть HashMap в моей программе, к которой обращаются несколько потоков, и иногда она устанавливается одним потоком.

Например:

Map<String, String> myMap = new HashMap<String, String>();

Доступ к нему осуществляется несколькими потоками. Один раз в час один поток вызывает:

myMap = myRefreshedVersionOfTheMap;

Итак, мой вопрос в том, является ли это потокобезопасным. Если обе карты всегда имеют ключ "importantKey", может ли поток чтения когда-либо получить доступ к карте в то время, когда "importantKey" не существует?

Редактировать:

Благодаря ответам я понял, что этот вопрос фактически не зависит от HashMap. Это был больше вопрос о присвоении ссылки на объект.


person CorayThan    schedule 20.06.2013    source источник
comment
возможный дубликат в Java, безопасно ли изменять ссылку на одновременное чтение HashMap   -  person Raedwald    schedule 21.06.2013


Ответы (4)


Это не потокобезопасный. Несмотря на то, что после момента публикации в самой карте нет записей (с точки зрения потока, выполняющего публикацию), а назначение ссылки является атомарным, новый Map<> не был безопасно опубликован . В частности, записи в карту во время ее создания - либо в конструкторе, либо после, в зависимости от того, как вы добавляете эти элементы, и эти записи могут или не могут быть замечены другими потоками, поскольку хотя они интуитивно возникают до того, как карта будет опубликована в других потоках, формально это не так в соответствии с моделью памяти.

Чтобы объект был безопасно опубликован, он должен быть передан внешнему миру с помощью некоторого механизма, который либо устанавливает связь «происходит раньше» между конструкцией объекта, публикацией ссылки и прочитанной ссылкой, либо он должен использовать несколько более узких методов, которые гарантированно безопасны для публикации:

  • Инициализация ссылки на объект из статического инициализатора.
  • Сохранение ссылки на него в финальном поле.

Ваша идиома будет в безопасности, если вы объявите myMap volatile. Более подробную информацию о безопасной публикации можно найти в JCIP ( настоятельно рекомендуется), или здесь, или в этом более подробный ответ на аналогичную тему.

person BeeOnRope    schedule 20.06.2013
comment
+1, это правильно. Я думаю, что одна вещь, которая движет этим домом, - это строка из статьи в Википедии о модели памяти Java : На современных платформах код часто выполняется не в том порядке, в котором он был написан. Ваш код может инициализировать новую карту, ЗАТЕМ установить ссылку, но нет гарантии, что JVM сохранит ее таким образом !! - person sharakan; 20.06.2013
comment
Вы говорите, что у меня может быть метод, который вызывает Map myRefreshedVersionOfMap = null;, за которым следует myRefreshedVersionOfMap = new HashMap(); myMap = myRefreshedVersionOfMap;, но поскольку код часто не выполняется в том порядке, в котором он был написан, другие потоки, обращающиеся к myMap, могут видеть его как null в какой-то момент? Это бессмысленно. Все это делается в одном потоке, одним методом. Если я помещаю значения в карту до того, как назначу myMap равным myRefreshedMap, вы говорите, что другие потоки могли видеть старую версию myRefreshedMap, которая существовала до назначения? - person CorayThan; 20.06.2013
comment
@CorayThan Не совсем так. myMap может иметь ненулевую ссылку на память, выделенную для HashMap, но конструктор HashMap еще не был запущен. Если другой поток обращается к этой ячейке памяти после этой ссылки, но до этот поток запускает конструктор, тогда произойдут Плохие вещи. - person sharakan; 20.06.2013
comment
@CorayThan проверить stackoverflow.com / questions / 16213443 / - person sharakan; 21.06.2013
comment
@CorayThan - не совсем так. Вам даже не нужно когда-либо устанавливать значение null, и вам не нужно все это с myRefreshedVersionOfMap (добавление не имеет значения). Я говорю, что если вы инициализируете myMap при построении, скажем, одним ключом-значением foo = bar, а затем позже другим набором потока myMap = otherMap, где otherMap был создан как otherMap = new HashMap(); otherMap.put("foo", "dog"), вы ожидаете, что каждый поток либо увидит карту с foo = bar , или foo = dog. На самом деле, однако, вы могли видеть карту вообще без ключей, карту с нулевым ключом, карту с foo = null. - person BeeOnRope; 21.06.2013
comment
... или карта, которая выдает NPE, когда вы вызываете get (), или карту, которая никогда не возвращается из get (это часто случается в реальной жизни и т. д. и т. д.). Наивные концепции этого случаются до того, как в одном потоке не применяются напрямую, когда вы говорите о нескольких потоках. Вы не можете рассуждать о порядке событий, как будто они имеют некий глобальный сериализованный порядок. Два потока могут видеть одни и те же два события в противоположном порядке (даже если вы думаете, что даже если порядок был случайным, он должен иметь некоторый фиксированный порядок - это не так). - person BeeOnRope; 21.06.2013
comment
Потоки могут видеть невозможный порядок, например, пустую хеш-карту, даже если вы устанавливаете глобальную ссылку только после того, как она была непустой и т. Д. Вы можете рассуждать об этих вещах только через формальную структуру, предлагаемую JMM. Опять же, я настоятельно рекомендую выбрать JCIP - деньги потрачены не зря, но если вам нужен бесплатный вариант, начните с этот пост и двигайтесь вперед во времени. Джереми и его компания в основном написали формальную спецификацию JMM, так что вы можете считать ее авторитетной. - person BeeOnRope; 21.06.2013
comment
Я до сих пор не читал JCIP, но я только что закончил «Эффективное Java 2-е изд.», И пункт 66 содержит пример, показывающий, почему здесь требуется volatile. - person CorayThan; 07.07.2014
comment
@CorayThan - вы также можете ознакомиться с этим ответом, в котором содержится более подробная информация и несколько полезных ссылок. - person BeeOnRope; 02.02.2017

Если вы имеете в виду, что создаете совершенно новый Map и назначаете его myMap, к которому обращаются другие потоки, тогда да. Назначение ссылок является атомарным. Это потокобезопасно, потому что вы не изменяете содержимое Map, пока другие потоки читают из него - у вас просто есть несколько потоков, читающих из Map.

Вам просто нужно объявить его volatile, чтобы другие потоки не кэшировали его.

person Brian Roach    schedule 20.06.2013
comment
В этом случае карта является свойством компонента Spring Singleton-scoped, поэтому я не верю, что у него могут быть такие проблемы с кешированием, верно? - person CorayThan; 20.06.2013
comment
Даже если класс, содержащий Map, является синглтоном, вам понадобится volatile; то, что он является синглтоном, означает, что существует (гарантированный) только один экземпляр этого класса, совместно используемый вашими потоками. Это ничем не отличается от доступа нескольких потоков к одному экземпляру любого другого класса. Когда вы назначаете этот новый Map, другие потоки иначе его не увидят. - person Brian Roach; 20.06.2013
comment
Это не так. Присвоение ссылки атомарно просто означает, что потоки не могут видеть фиктивную ссылку (никуда не указывающую, т. Е. Нарушающую модель безопасности JVM). Это не гарантирует, что карта ‹›, на которую указывает ссылка, полностью построена. Другой поток может увидеть ссылку на новую карту, но затем получить доступ к карте ‹›, которая построена только частично, с неизвестными эффектами. volatile требуется здесь для безопасной публикации, а не только во избежание кеширования. - person BeeOnRope; 20.06.2013
comment
ТАК ты имеешь в виду, что именно мой ответ говорит? О нет, подождите, вы просто педантичны по поводу зачем вам нужен volatile. Продолжать. - person Brian Roach; 20.06.2013
comment
Хм? Весь ваш ответ неверен - рассуждения в первом абзаце совершенно неверны, это небезопасно. - person BeeOnRope; 20.06.2013
comment
Вы случайно упомянули volatile, чтобы избежать его кэширования другими потоками - другими словами, он все еще безопасен, но вы можете увидеть устаревшие значения из-за кеширования. Это неверно - это совершенно небезопасно, независимо от того, устаревшие значения или нет. Ваша программа может завершиться, вести себя нестандартно, зайти в бесконечный цикл и т. Д. - person BeeOnRope; 20.06.2013
comment
В частности, ваш ответ на второй вопрос, заданный OP: если обе карты всегда имеют ключ importantKey, возможно ли, чтобы поток чтения когда-либо обращался к карте в то время, когда importantKey не существует? - Нет, но у меня - Да, плюс у него может быть другое произвольное плохое поведение. - person BeeOnRope; 20.06.2013
comment
FWIW, вот почти идентичный вопрос, и выбранный ответ, и любой другой ответ также неверен в отношении поведения, если volatile отсутствует. Это тонкий момент, который часто упускают. Атомарность записи по ссылке ничего не гарантирует относительно содержимого объекта, на который указывает ссылка. - person BeeOnRope; 20.06.2013
comment
Когда я сказал, что когда-нибудь возможно, я не имел в виду включить ситуацию, когда космические лучи меняют биты и делают карту нулевой, или другие столь же радикальные ситуации, как завершение программы. - person CorayThan; 20.06.2013
comment
Не волнуйтесь, я тоже не об этом говорил. В нормально работающей JVM ваша программа вполне может прочитать карту и не увидеть этот ключ, даже если каждая карта, назначенная этой ссылке, всегда имела этот ключ. Это тонко, но важно. Подробности см. В моем ответе ниже. Фактически, вы можете воспроизвести этот или аналогичный выпуск безопасных публикаций на месте - космические лучи не нужны. - person BeeOnRope; 20.06.2013
comment
@BeeOnRope прав насчет этого. Если бы инструкции в вашем коде выполнялись в том порядке, в котором они были написаны, все будет в порядке. Но нет никакой гарантии, что это произойдет. Неустойчивая или другая конструкция синхронизации необходима, чтобы обеспечить инициализацию карты happens-before назначением ссылки. - person sharakan; 20.06.2013
comment
@BeeOnRope, хотя вы правы, изменчивая запись обеспечивает одновременно два типа видимости - (а) сама запись и (б) запись перед ней, поэтому не случайно люди, нацеленные на (а), получают выгоду от (б ) тоже. - person ZhongYu; 20.06.2013
comment
Согласен, но важно понимать разницу, потому что бывают случаи, когда вам не важна видимость, но вы всегда заботитесь о правильности. Совершенно неправильно утверждать, что это безопасно, за исключением проблем с видимостью. Фактически, OP специально проводит различие между видимостью и безопасностью, когда спрашивает о importantKey. Одна из причин не заботиться о видимости - это когда вы знаете, что в конечном итоге между потоками будет какое-то другое действие по синхронизации, поэтому период, в течение которого вы можете видеть устаревшие значения, ограничен. - person BeeOnRope; 21.06.2013
comment
Еще одна причина, по которой вам это может быть интересно, заключается в том, что вам нужно знать гарантии, связанные с небезопасной публикацией и неизменяемыми объектами, например, по соображениям безопасности (например, String разработан специально для смягчения этих проблем). - person BeeOnRope; 21.06.2013
comment
@CorayThan - надеюсь, вы понимаете, что использование volatile имеет решающее значение для корректности этого решения. - person jtahlborn; 21.06.2013
comment
Что ж, похоже, ОП просто искал ответ, который подтвердил его интуитивное понимание того, как это должно работать, а не истину. Показывает некоторые риски, связанные с тем, чтобы говорить ТАКИЕ ответы как Евангелие (хотя я совершаю ту же ошибку). Наверняка любой, кто читает это? N, просто посмотрит на первый ответ и что-нибудь, о, это безопасно без изменчивости, если меня не волнуют устаревшие значения. - person BeeOnRope; 21.06.2013
comment
@CorayThan Вы упускаете суть, риск не крошечный шанс несогласованности, вы можете получить полностью сломанную карту, которая генерирует исключения, которые никогда не должны были быть выброшены. volatile не увеличивает сложность и практически не влияет на производительность. В этом примере карта должна быть изменчивой. - person assylias; 30.07.2013
comment
Как бы то ни было, если вас устраивает крошечный шанс несоответствия, вы должны указать это заранее в своем вопросе (и, по сути, вы потратите следующие несколько дней на определение tiny ) и не задавать чёрно-белые вопросы, например, это потокобезопасно. Что-то с любой вероятностью несоответствия (и на самом деле, как указывает @assylias, которое охватывает весь спектр неопределенного поведения) по определению не является потокобезопасным. - person BeeOnRope; 09.10.2013

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

Однако, поскольку чтение и запись в ссылки в Java являются атомарными, описанный вами шаблон может быть потокобезопасным, если код обновления не изменяет старую карту. Например, подойдет следующее:

// this refresh code would be thread-safe
Map<String, String> copy = new HashMap<String, String>(myMap);
copy.put(x, y); // change the map
myMap = copy;

// you might also consider
myMap = Collections.unmodifiableMap(copy);
// to make sure that the non-thread-safe map will never be mutated

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

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

person ChaseMedallion    schedule 20.06.2013
comment
По сути, это то, что я сделал. В методе обновления я полностью создаю действительную заменяющую карту, а затем назначаю ее за одну операцию. - person CorayThan; 20.06.2013
comment
Это неверно. Несмотря на то, что нет никаких операций чтения или записи, новые HashMaps не безопасно публикуются для внешнего мира. Читающие потоки могут видеть новые карты HashMap в неопределенном состоянии. - person BeeOnRope; 20.06.2013
comment
@BeeOnRope: хотя я мог видеть, что это верно для произвольной структуры данных, я не уверен, что это верно для java HashMap. В документации особо указывается на необходимость синхронизации в случае изменения карты: если несколько потоков одновременно обращаются к хэш-карте, и хотя бы один из потоков структурно изменяет карту, она должна быть синхронизирована извне (из docs.oracle.com/javase/6/docs/api/java/util /HashMap.html) - person ChaseMedallion; 21.06.2013
comment
Эта документация просто объясняет, что HashMap не является потокобезопасным в целом (например, в отличие от HashTable). Он не говорит о безопасной публикации и не заменяет понимание этой концепции. Правила безопасной публикации универсальны и применяются ко всем классам, включая HashMap. Также стоит отметить, что этот текст существовала даже до того, как модель памяти была представлена ​​в Java 5. Я бы также не сказал, что комментарии в javadoc устанавливают необходимое условие для безопасности потоков, но не достаточное. - person BeeOnRope; 21.06.2013

HashMap не является потокобезопасным. Вы можете использовать любое из следующих

  1. ConcurrentHashMap.
  2. HashMap с синхронизацией снаружи.
  3. Разная HashMap для каждого потока.

Проверьте этот аналогичный ответ здесь

person stinepike    schedule 20.06.2013
comment
Похоже, он не об этом спрашивает; у него нет одновременных операций чтения / записи, только чтение и назначение ссылки. - person Brian Roach; 20.06.2013