Безопасно ли получать значения из java.util.HashMap из нескольких потоков (без изменений)?
Существует случай, когда будет построена карта, и после ее инициализации она больше не будет изменена. Однако он будет доступен (только с помощью get (key)) из нескольких потоков. Безопасно ли использовать java.util.HashMap
таким образом?
(В настоящее время я с радостью использую java.util.concurrent.ConcurrentHashMap
и не требую значительных усилий для повышения производительности, но мне просто интересно, достаточно ли простого HashMap
. Следовательно, этот вопрос не является "Какой из них я должен использовать?", и это не вопрос производительности. Вместо этого возникает вопрос: "Было бы безопасно?" )
Ответы
Ответ 1
Ваша идиома безопасна , если и только если ссылка на HashMap
безопасно опубликована. В отличие от всего, что связано с внутренними компонентами HashMap
, безопасная публикация посвящена тому, как конструирующий поток делает ссылку на карту видимой для других потоков.
В принципе, единственная возможная гонка здесь находится между конструкцией HashMap
и любыми потоками чтения, которые могут получить к ней доступ до того, как она будет полностью построена. Большая часть обсуждения касается того, что происходит с состоянием объекта карты, но это не имеет значения, поскольку вы никогда его не модифицируете, поэтому единственной интересной частью является публикация ссылки HashMap
.
Например, представьте, что вы публикуете такую карту:
class SomeClass {
public static HashMap<Object, Object> MAP;
public synchronized static setMap(HashMap<Object, Object> m) {
MAP = m;
}
}
... и в какой-то момент setMap()
вызывается с картой, а другие потоки используют SomeClass.MAP
для доступа к карте и проверяют значение null следующим образом:
HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
.. use the map
} else {
.. some default behavior
}
Это не безопасно, хотя оно, вероятно, похоже на то, что оно есть. Проблема в том, что между SomeObject.MAP
и последующим чтением в другом потоке не существует before-before, поэтому поток чтения чтобы увидеть частично построенную карту. Это может в значительной степени сделать что угодно, и даже на практике он делает такие вещи, как помещает поток чтения в бесконечный цикл.
Чтобы безопасно опубликовать карту, вам необходимо установить связь между событиями между записью ссылки на HashMap
(т.е. публикацией) и последующими читателями этой ссылки (то есть, потреблением). Удобно, есть только несколько простых способов запомнить выполнить, что [1]:
- Обмен ссылками через правильно заблокированное поле (JLS 17.4.5)
- Используйте статический инициализатор для создания хранилищ инициализации (JLS 12.4)
- Обмен ссылкой через поле volatile (JLS 17.4.5) или как следствие этого правила через классы AtomicX
- Инициализировать значение в конечном поле (JLS 17.5).
Наиболее интересными для вашего сценария являются (2), (3) и (4). В частности, (3) применяется непосредственно к указанному выше коду: если вы преобразуете объявление MAP
в:
public static volatile HashMap<Object, Object> MAP;
тогда все будет кошерным: читатели, которые видят ненулевое значение, обязательно имеют отношение до отношения с хранилищем к MAP
и, следовательно, видят все магазины, связанные с инициализацией карты.
Другие методы изменяют семантику вашего метода, поскольку оба (2) (используя статический инициализатор) и (4) (используя final) подразумевают, что вы не можете динамически установить MAP
во время выполнения. Если вам не нужно это делать, просто объявите MAP
как static final HashMap<>
, и вам гарантирована безопасная публикация.
На практике правила просты для безопасного доступа к "никогда не модифицированным объектам":
Если вы публикуете объект, который по своей сути не является неизменным (как во всех объявленных объявлении final
) и:
- Вы уже можете создать объект, который будет назначен в момент объявления a: просто используйте поле
final
(включая static final
для статических членов).
- Вы хотите назначить объект позже, после того, как ссылка уже видна: используйте поле volatile b.
Что это!
На практике это очень эффективно. Например, использование поля static final
позволяет JVM предположить, что значение не изменяется для срока службы программы и оптимизирует ее. Использование поля элемента final
позволяет большинству архитектур читать поле таким же образом, как и нормальное поле, и не препятствует дальнейшим оптимизации c.
Наконец, использование volatile
имеет какое-то влияние: на многих архитектурах (например, x86, особенно на тех, которые не позволяют чтению читать сообщения) не требуется аппаратный барьер, но может не произойти некоторая оптимизация и переупорядочение во время компиляции, но этот эффект обычно мал. Взамен, вы на самом деле получаете больше, чем вы просили - вы можете не только безопасно опубликовать один HashMap
, вы можете сохранить как можно больше не модифицированных HashMap
, как вы хотите, с той же ссылкой и быть уверены, что все читатели будут см. безопасно опубликованную карту.
Подробнее о подробностях см. Shipilev или этот FAQ Мэнсон и Гетц.
[1] Прямо цитируя Shipilev.
a Это звучит сложно, но я имею в виду, что вы можете назначить ссылку во время построения - либо в точке объявления, либо в конструкторе (поля-члены) или статическом инициализаторе (статические поля).
b По желанию вы можете использовать метод synchronized
для получения/установки или AtomicReference
или что-то еще, но мы говорим о минимальной работе, которую вы можете сделать.
c Некоторые архитектуры с очень слабыми моделями памяти (я смотрю на вас, Alpha) могут потребовать некоторый тип считывающего барьера перед чтением final
, но они очень редки сегодня.
Ответ 2
Джереми Мэнсон, бог, когда дело доходит до модели памяти Java, имеет три части блога на эту тему - потому что в основном вы задаете вопрос "Безопасно ли получить доступ к неизменяемой HashMap" - ответ на этот вопрос да. Но вы должны ответить на предикат на тот вопрос, который есть - "Является ли мой HashMap неизменным". Ответ может вас удивить: у Java есть относительно сложный набор правил для определения неизменяемости.
За дополнительной информацией по этой теме читайте сообщения в блоге Джереми:
Часть 1 о неизменности в Java:
http://jeremymanson.blogspot.com/2008/04/immutability-in-java.html
Часть 2 о неизменности в Java:
http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-2.html
Часть 3 о неизменности в Java:
http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-3.html
Ответ 3
Чтение безопасно с точки зрения синхронизации, но не с точки зрения памяти. Это то, что широко не понято среди разработчиков Java, включая здесь, в Stackoverflow. (Соблюдайте рейтинг этого ответа для подтверждения.)
Если у вас есть другие потоки, они могут не увидеть обновленную копию HashMap, если нет записи памяти из текущего потока. Запись в память происходит с помощью синхронизированных или изменчивых ключевых слов или путем использования некоторых конструкций java concurrency.
Подробнее см. статью Брайана Гетца о новой модели памяти Java.
Ответ 4
После немного большего поиска я нашел это в java doc (внимание мое):
Обратите внимание, что эта реализация не синхронизированы. Если несколько потоков доступ к карте хешей одновременно, и при по меньшей мере один из потоков изменяет Карта структурно, она должна быть синхронизированный снаружи. (структурный модификация - любая операция, которая добавляет или удаляет одно или несколько сопоставлений; просто изменение связанного значения с ключом, который уже имеет экземпляр не является структурным модификация.)
Это, по-видимому, означает, что это будет безопасно, если предположить, что обратное утверждение истинно.
Ответ 5
Однако есть важный поворот. Он безопасен для доступа к карте, но в целом он не гарантирует, что все потоки будут видеть точно такое же состояние (и, следовательно, значения) HashMap. Это может произойти в многопроцессорных системах, где изменения в HashMap, выполненные одним потоком (например, тот, который его заполняет), могут находиться в этом кэше ЦП и не будут отображаться потоками, запущенными на других ЦП, до тех пор, пока операция забора памяти не будет выполнял обеспечение согласованности кеша. Спецификация языка Java явна в этом отношении: решение состоит в том, чтобы получить блокировку (синхронизированную (...)), которая испускает операцию забора памяти. Итак, если вы уверены, что после заполнения HashMap каждый из потоков получает ЛЮБОЙ замок, тогда с этого момента он будет в порядке, чтобы получить доступ к HashMap из любого потока, пока HashMap не будет изменен снова.
Ответ 6
Следует отметить, что при некоторых обстоятельствах get() из несинхронизированного HashMap может вызвать бесконечный цикл. Это может произойти, если параллельный put() вызывает передел карты.
http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html
Ответ 7
В соответствии с http://www.ibm.com/developerworks/java/library/j-jtp03304/ # Безопасность инициализации вы можете сделать свое HashMap окончательным полем, а после завершения конструктора он будет безопасно опубликован.
...
В новой модели памяти есть что-то похожее на связь между записью конечного поля в конструкторе и начальной загрузкой общей ссылки на этот объект в другом потоке.
...
Ответ 8
Итак, сценарий, который вы описываете, состоит в том, что вам нужно поместить кучу данных в карту, а затем, когда вы закончите заполнение, вы считаете ее неизменной. Один из подходов, который является "безопасным" (что означает, что вы применяете его, чтобы он действительно считался неизменным) заключается в замене ссылки на Collections.unmodifiableMap(originalMap), когда вы будете готовы сделать ее неизменной.
Пример того, как плохие карты могут сбой, если они используются одновременно, и предлагаемый обходной путь, о котором я упоминал, проверьте эту запись парада ошибок: bug_id=6423457
Ответ 9
Следует предупредить, что даже в однопоточном коде замена ConcurrentHashMap на HashMap может быть небезопасной. ConcurrentHashMap запрещает null как ключ или значение. HashMap не запрещает их (не спрашивайте).
Итак, в маловероятной ситуации, когда ваш существующий код может добавить нуль в коллекцию во время установки (предположительно в случае сбоя какого-либо рода), замена коллекции, как описано, изменит функциональное поведение.
Тем не менее, если вы ничего не делаете, одновременные чтения из HashMap безопасны.
[Edit: by "concurrent reads", я имею в виду, что не допускаются одновременные модификации.
Другие ответы объясняют, как обеспечить это. Один из способов - сделать карту неизменной, но это не обязательно. Например, модель памяти JSR133 явно определяет начало потока как синхронизированное действие, что означает, что изменения, сделанные в потоке A до начала потока B, видны в потоке B.
Мое намерение не противоречить тем более подробным ответам о модели памяти Java. Этот ответ должен указывать на то, что даже помимо проблем concurrency существует по меньшей мере одно различие API между ConcurrentHashMap и HashMap, которое может отскакивать даже однопоточную программу, которая заменила ее на другую.]
Ответ 10
http://www.docjar.com/html/api/java/util/HashMap.java.html
вот источник для HashMap. Как вы можете сказать, там нет кода блокировки/мьютекса.
Это означает, что, хотя его хорошо читать с HashMap в многопоточной ситуации, я бы определенно использовал ConcurrentHashMap, если было несколько записей.
Интересно, что и .NET HashTable, и Dictionary < K, V > имеют встроенный код синхронизации.
Ответ 11
Если инициализация и каждый столбец синхронизированы, вы сохраняете.
Следующий код сохраняется, потому что загрузчик классов позаботится о синхронизации:
public static final HashMap<String, String> map = new HashMap<>();
static {
map.put("A","A");
}
Следующий код сохраняется, потому что запись volatile позаботится о синхронизации.
class Foo {
volatile HashMap<String, String> map;
public void init() {
final HashMap<String, String> tmp = new HashMap<>();
tmp.put("A","A");
// writing to volatile has to be after the modification of the map
this.map = tmp;
}
}
Это также будет работать, если переменная-член является окончательной, поскольку final также нестабилен. И если метод является конструктором.