Безопасно ли получать значения из java.util.HashMap из нескольких потоков (без изменений)?
Существует случай, когда карта будет построена, и после ее инициализации она никогда не будет изменена снова. Однако к нему можно будет получить доступ (только через get(key)) из нескольких потоков. Безопасно ли использовать java.util.HashMap
в этом случае?
(В настоящее время я с удовольствием использую java.util.concurrent.ConcurrentHashMap
и не имеют никакой необходимости улучшать производительность, но мне просто любопытно, если HashMap
было бы достаточно. Следовательно, этот вопрос не "Какой из них я должен использовать?" и это не вопрос производительности. Скорее, вопрос "это будет безопасно?")
11 ответов
Ваша идиома безопасна тогда и только тогда, когда ссылка на 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
чтобы получить доступ к карте, и проверить на ноль, как это:
HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
.. use the map
} else {
.. some default behavior
}
Это небезопасно, даже если кажется, что это так. Проблема в том, что между множеством SomeObject.MAP
и последующее чтение в другом потоке, поэтому поток чтения может видеть частично построенную карту. Это может в значительной степени делать что угодно, и даже на практике это делает такие вещи, как помещение потока чтения в бесконечный цикл.
Чтобы безопасно опубликовать карту, вам нужно установить связь между написанием ссылки на HashMap
(т. е. публикация) и последующие читатели этой ссылки (т. е. потребление). Удобно, что есть только несколько простых для запоминания способов сделать это [1]:
- Обмен ссылками через правильно заблокированное поле ( JLS 17.4.5)
- Используйте статический инициализатор для инициализации хранилищ ( JLS 12.4)
- Обмен ссылками через изменчивое поле ( JLS 17.4.5) или, как следствие этого правила, через классы AtomicX
- Инициализируйте значение в конечном поле ( JLS 17.5).
Наиболее интересными для вашего сценария являются (2), (3) и (4). В частности, (3) применяется непосредственно к коду, который я имею выше: если вы преобразуете декларацию MAP
чтобы:
public static volatile HashMap<Object, Object> MAP;
тогда все становится кошернее: читатели, которые видят ненулевое значение, обязательно имеют отношения " до и с магазином", чтобы MAP
и, следовательно, увидеть все магазины, связанные с инициализацией карты.
Другие методы изменяют семантику вашего метода, так как оба (2) (используя статический инициализатор) и (4) (используя финальный) подразумевают, что вы не можете установить MAP
динамически во время выполнения. Если вам не нужно этого делать, просто объявите MAP
как static final HashMap<>
и вам гарантирована безопасная публикация.
На практике правила просты для безопасного доступа к "неизмененным объектам":
Если вы публикуете объект, который не является неизменным (как во всех объявленных полях) final
) а также:
- Вы уже можете создать объект, который будет назначен в момент объявления a: просто используйте a
final
поле (в том числеstatic final
для статических членов). - Вы хотите назначить объект позже, после того как ссылка уже видна: используйте изменяемое поле b.
Это оно!
На практике это очень эффективно. Использование static final
Поле, например, позволяет JVM предполагать, что значение не изменяется в течение всего жизненного цикла программы, и сильно его оптимизировать. Использование final
Поле member позволяет большинству архитектур читать поле способом, эквивалентным нормальному чтению поля, и не препятствует дальнейшей оптимизации c.
Наконец, использование volatile
действительно оказывает определенное влияние: во многих архитектурах не требуется аппаратный барьер (например, x86, особенно те, которые не позволяют чтению передавать чтение), но некоторая оптимизация и переупорядочение могут не произойти во время компиляции - но этот эффект обычно невелик. Взамен вы фактически получаете больше, чем просили - вы можете не только безопасно опубликовать один HashMap
Вы можете хранить как можно больше немодифицированных HashMap
Если вы хотите использовать ту же ссылку и быть уверенным, что все читатели увидят благополучно опубликованную карту.
Для получения более подробной информации обратитесь к Шипилеву или к часто задаваемым вопросам Мэнсона и Гетца.
[1] Напрямую цитирую Шипилева.
Это звучит сложно, но я имею в виду, что вы можете назначить ссылку во время построения - либо в точке объявления, либо в конструкторе (поля-члены) или статическом инициализаторе (статические поля).
b При желании вы можете использовать synchronized
метод получить / установить, или AtomicReference
или что-то, но мы говорим о минимальной работе, которую вы можете сделать.
c Некоторые архитектуры с очень слабыми моделями памяти (я смотрю на вас, Альфа) могут потребовать некоторый тип барьера чтения перед final
читать - но это очень редко сегодня.
Джереми Мэнсон, бог, когда дело доходит до модели памяти 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
Чтения безопасны с точки зрения синхронизации, но не с точки зрения памяти. Это то, что неправильно понимают разработчики Java, в том числе и здесь, на Stackru. (Обратите внимание на оценку этого ответа для доказательства.)
Если у вас запущены другие потоки, они могут не увидеть обновленную копию HashMap, если в текущий поток не записана память. Запись в память происходит посредством использования синхронизированных или изменчивых ключевых слов или с использованием некоторых конструкций параллелизма Java.
Подробности смотрите в статье Брайана Гетца о новой модели памяти Java.
Немного посмотрев, я нашел это в java doc (выделено мной):
Обратите внимание, что эта реализация не синхронизирована. Если несколько потоков обращаются к хэш-карте одновременно, и хотя бы один из потоков структурно изменяет карту, она должна быть синхронизирована извне. (Структурная модификация - это любая операция, которая добавляет или удаляет одно или несколько сопоставлений; простое изменение значения, связанного с ключом, который уже содержится в экземпляре, не является структурной модификацией.)
Похоже, это подразумевает, что это будет безопасно, если обратное утверждение там верно.
Следует отметить, что при некоторых обстоятельствах get() из несинхронизированного HashMap может вызвать бесконечный цикл. Это может произойти, если одновременный метод put() вызывает перефразирование карты.
http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html
Есть важный поворот, хотя. Доступ к карте безопасен, но в целом не гарантируется, что все потоки будут видеть одно и то же состояние (и, следовательно, значения) HashMap. Это может произойти в многопроцессорных системах, где изменения в HashMap, сделанные одним потоком (например, тем, который его заполнил), могут находиться в кэше этого ЦП и не будут видны потокам, работающим на других ЦП, пока операция ограничения памяти не будет выполнено обеспечение согласованности кэша. Спецификация языка Java в этом однозначна: решение состоит в том, чтобы получить блокировку (synchronized (...)), которая испускает операцию ограничения памяти. Итак, если вы уверены, что после заполнения HashMap каждый из потоков получает ЛЮБУЮ блокировку, то с этого момента можно получить доступ к HashMap из любого потока, пока HashMap снова не будет изменен.
В соответствии с http://www.ibm.com/developerworks/java/library/j-jtp03304/ # Безопасностью инициализации вы можете сделать ваш HashMap конечным полем, и после завершения работы конструктора он будет безопасно опубликован.
... В новой модели памяти существует нечто похожее на отношение "происходит до" между записью конечного поля в конструкторе и начальной загрузкой общей ссылки на этот объект в другом потоке....
Этот вопрос рассматривается в книге Брайана Гетца "Java Concurrency in Practice" (Листинг 16.8, стр. 350):
@ThreadSafe
public class SafeStates {
private final Map<String, String> states;
public SafeStates() {
states = new HashMap<String, String>();
states.put("alaska", "AK");
states.put("alabama", "AL");
...
states.put("wyoming", "WY");
}
public String getAbbreviation(String s) {
return states.get(s);
}
}
поскольку states
объявлен как final
и его инициализация выполняется в конструкторе класса владельца, любой поток, который позже прочитает эту карту, гарантированно увидит ее на момент завершения работы конструктора, при условии, что никакой другой поток не попытается изменить содержимое карты.
Итак, сценарий, который вы описали, заключается в том, что вам нужно поместить кучу данных в карту, а затем, когда вы закончите заполнять ее, вы будете считать ее неизменной. Один из подходов, который является "безопасным" (имеется в виду, что вы действительно используете его как неизменяемый), заключается в замене ссылки на Collections.unmodifiableMap(originalMap)
когда вы будете готовы сделать его неизменным.
Для примера того, как плохо карты могут потерпеть неудачу при одновременном использовании, и предлагаемый обходной путь, который я упомянул, проверьте эту запись парада ошибок: bug_id = 6423457
Имейте в виду, что даже в однопоточном коде замена ConcurrentHashMap на HashMap может быть небезопасной. ConcurrentHashMap запрещает нуль как ключ или значение. HashMap не запрещает их (не спрашивайте).
Таким образом, в маловероятной ситуации, когда ваш существующий код может добавить ноль в коллекцию во время установки (предположительно в случае сбоя какого-либо типа), замена коллекции, как описано, изменит функциональное поведение.
Тем не менее, при условии, что вы больше ничего не делаете, параллельные чтения из HashMap безопасны.
[Редактировать: под "одновременным чтением" я подразумеваю, что не существует и одновременных модификаций.
Другие ответы объясняют, как это обеспечить. Один из способов - сделать карту неизменной, но это не обязательно. Например, модель памяти JSR133 явно определяет запуск потока как синхронизированного действия, а это означает, что изменения, сделанные в потоке A до его запуска, видны в потоке B.
Я не намерен противоречить более подробным ответам о модели памяти Java. Этот ответ предназначен для того, чтобы указать, что, несмотря на проблемы с параллелизмом, между ConcurrentHashMap и HashMap есть, по крайней мере, одно различие в API, которое может убрать даже однопоточную программу, которая заменит одну на другую.]
Если инициализация и каждый путь синхронизированы, вы сохранены.
Следующий код сохранен, потому что загрузчик классов позаботится о синхронизации:
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 также является volatile. И если метод является конструктором.
http://www.docjar.com/html/api/java/util/HashMap.java.html
Вот источник для HashMap. Как вы можете сказать, там нет абсолютно никакого кода блокировки / мьютекса.
Это означает, что хотя чтение из HashMap в многопоточной ситуации вполне нормально, я бы определенно использовал ConcurrentHashMap, если было несколько записей.
Что интересно, в.NET HashTable и Dictionary