Безопасно ли получать значения из java.util.HashMap из нескольких потоков (без изменений)?

143

Есть случай, когда карта будет построена, и после инициализации она больше никогда не будет изменена. Однако к нему можно будет получить доступ (только с помощью get (key)) из нескольких потоков. Насколько безопасно использовать java.util.HashMapтакой способ?

(В настоящее время я с удовольствием использую a java.util.concurrent.ConcurrentHashMap, и у меня нет никакой потребности в повышении производительности, но мне просто любопытно, достаточно ли простого HashMap. Следовательно, этот вопрос не «Какой из них мне использовать?», И это не вопрос производительности. Скорее вопрос: «Будет ли это безопасно?»)

Дэйв Л.
источник
4
Многие ответы здесь верны в отношении взаимного исключения из запущенных потоков, но неверны в отношении обновлений памяти. Я проголосовал за / против соответственно, но все еще есть много неправильных ответов с положительными голосами.
Хит Бордерс,
@Heath Borders, если экземпляр a был статически инициализирован немодифицируемым HashMap, он должен быть безопасным для одновременного чтения (поскольку другие потоки не могли пропустить обновления, поскольку обновлений не было), верно?
kaqqao
Если он статически инициализирован и никогда не изменялся за пределами статического блока, тогда это может быть нормально, потому что вся статическая инициализация синхронизируется с помощью ClassLoader. Это стоит отдельного вопроса. Я бы все равно явно синхронизировал его и профиль, чтобы убедиться, что это вызывает реальные проблемы с производительностью.
Heath Borders
@HeathBorders - что вы имеете в виду под "обновлениями памяти"? JVM - это формальная модель, которая определяет такие вещи, как видимость, атомарность, отношения « происходит раньше» , но не использует такие термины, как «обновления памяти». Вы должны уточнить, желательно используя терминологию из JLS.
BeeOnRope 01
2
@Dave - Я полагаю, вы все еще не ищете ответа по прошествии 8 лет, но для записи основная путаница почти во всех ответах заключается в том, что они сосредоточены на действиях, которые вы выполняете над объектом карты . Вы уже объяснили, что никогда не изменяете объект, так что все это не имеет значения. Единственная потенциальная проблема - это то, как вы публикуете ссылку на Map, которую вы не объяснили. Если вы не сделаете это безопасно, это будет небезопасно. Если вы сделаете это безопасно, то так и будет . Подробности в моем ответе.
BeeOnRope 01

Ответы:

56

Ваша идиома безопасна тогда и только тогда , когда ссылка на 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 и последующим чтением в другом потоке , поэтому поток чтения может видеть частично построенную карту. Он может делать что угодно, и даже на практике он может помещать поток чтения в бесконечный цикл .

Чтобы безопасно опубликовать карту, вам необходимо установить происходит, прежде , чем отношения между написанием ссылки на HashMap(т. Е. Публикацией ) и последующими читателями этой ссылки (т. Е. Потреблением). Удобно, что есть только несколько легко запоминающихся способов сделать это [1] :

  1. Обмен ссылками через правильно заблокированное поле ( JLS 17.4.5 )
  2. Используйте статический инициализатор для инициализации хранилищ ( JLS 12.4 )
  3. Обмен ссылками через изменчивое поле ( JLS 17.4.5 ) или, как следствие этого правила, через классы AtomicX
  4. Инициализируйте значение в конечном поле ( JLS 17.5 ).

Наиболее интересными для вашего сценария являются (2), (3) и (4). В частности, (3) применяется непосредственно к приведенному выше коду: если вы преобразуете объявлениеMAP в:

public static volatile HashMap<Object, Object> MAP;

тогда все кошерно: читатели, которые видят ненулевое значение, обязательно имеют раньше» с магазином MAPи, следовательно, видят все магазины, связанные с инициализацией карты.

Другие методы изменяют семантику вашего метода, поскольку оба (2) (с использованием статического инициализатора) и (4) (с использованием final ) подразумевают, что вы не можете устанавливать MAPдинамически во время выполнения. Если тебе не нужно , просто объявитеMAP как a, static final HashMap<>и безопасная публикация вам гарантирована.

На практике правила просты для безопасного доступа к «неизменяемым объектам»:

Если вы публикуете объект, который не является неизменяемым по своей сути (как во всех объявленных поляхfinal ) и:

  • Вы уже можете создать объект, который будет назначен в момент объявления a : просто используйтеfinal поле (в том числе static finalдля статических членов).
  • Вы хотите назначить объект позже, когда ссылка уже будет видна: используйте изменчивое поле b .

Это оно!

На практике это очень эффективно. Использование static finalполя, например, позволяет JVM предполагать, что значение не меняется в течение всего срока службы программы, и сильно оптимизировать его. Использование поля- finalчлена позволяет большинству архитектур читать поле способом, эквивалентным нормальному чтению поля, и не препятствует дальнейшей оптимизации. c .

Наконец, использование volatileдействительно имеет некоторое влияние: аппаратный барьер не требуется на многих архитектурах (например, x86, особенно на тех, которые не разрешают чтение для передачи чтения), но некоторая оптимизация и переупорядочение могут не происходить во время компиляции - но это эффект вообще небольшой. Взамен вы действительно получаете больше, чем просили - вы можете не только безопасно опубликовать карту HashMap, но и сохранить столько немодифицированных HashMapфайлов, сколько захотите, в той же ссылке и быть уверенными, что все читатели увидят безопасно опубликованную карту. .

Для получения дополнительных сведений см. Шипилев или этот FAQ Мэнсона и Гетца .


[1] Прямая цитата из Шипилева .


а Это звучит сложно, но я имею в виду, что вы можете назначить ссылку во время создания - либо в точке объявления, либо в конструкторе (поля-члены), либо в статическом инициализаторе (статические поля).

b При желании вы можете использовать synchronizedметод для получения / установки илиAtomicReference или что-то еще, но мы говорим о минимальной работе, которую вы можете выполнить.

c В некоторых архитектурах с очень слабыми моделями памяти (я смотрю на вас , Alpha) может потребоваться какой-то тип барьера чтения перед finalчтением, но сегодня это очень редко.

BeeOnRope
источник
never modify HashMapstate of the map objectЯ не думаю, что это потокобезопасный. Бог знает реализацию библиотеки, если в официальном документе не сказано, что она потокобезопасна.
Jiang YD
@JiangYD - вы правы, в некоторых случаях там есть серая зона: когда мы говорим «изменить», мы на самом деле имеем в виду любое действие, которое внутренне выполняет некоторые записи, которые могут конкурировать с чтениями или записями в других потоках. Эти записи могут быть внутренними деталями реализации, поэтому даже операция, которая кажется «только для чтения», get()может на самом деле выполнять некоторые записи, например, обновление некоторой статистики (или в случае LinkedHashMapобновления порядка доступа по порядку доступа). Так что хорошо написанный класс должен содержать некоторую документацию, которая проясняет, если ...
BeeOnRope 03
... очевидно, что операции "только для чтения" действительно доступны только для чтения внутри в смысле безопасности потоков. В стандартной библиотеке C ++, например, существует общее правило, согласно которому отмеченные функции-члены constдействительно доступны только для чтения в этом смысле (внутри они все еще могут выполнять записи, но их необходимо сделать потокобезопасными). В constJava нет ключевого слова, и мне не известно о каких-либо задокументированных общих гарантиях, но в целом классы стандартной библиотеки ведут себя так, как ожидалось, а исключения задокументированы (см. LinkedHashMapПример, в котором операции RO getявно упоминаются как небезопасные).
BeeOnRope 03
@JiangYD - наконец, возвращаясь к вашему исходному вопросу, поскольку HashMapу нас действительно есть право в документации о поведении безопасности потоков для этого класса: если несколько потоков одновременно обращаются к хэш-карте, и по крайней мере один из потоков структурно изменяет карту, он должен быть синхронизирован внешне. (Структурная модификация - это любая операция, которая добавляет или удаляет одно или несколько сопоставлений; простое изменение значения, связанного с ключом, который уже содержится в экземпляре, не является структурной модификацией.)
BeeOnRope
Таким образом, HashMapметоды, которые, как мы предполагаем, будут доступны только для чтения, доступны только для чтения, поскольку они не изменяют структуру HashMap. Конечно, эта гарантия может не Mapдействовать для произвольных других реализаций, но вопрос HashMapконкретно в этом.
BeeOnRope 03
70

Джереми Мэнсон, бог, когда дело доходит до модели памяти 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
Это хороший момент, но я полагаюсь на статическую инициализацию, во время которой никакие ссылки не выходят, поэтому это должно быть безопасно.
Дэйв Л.
5
Я не понимаю, насколько это высоко оцененный ответ (или даже ответ). Во-первых, он даже не отвечает на вопрос и не упоминает один ключевой принцип, который будет определять, безопасно это или нет: безопасная публикация . «Ответ» сводится к «хитрости», и вот три (сложные) ссылки, которые вы можете прочитать.
BeeOnRope
Он отвечает на вопрос в самом конце первого предложения. Что касается ответа, он поднимает вопрос о том, что неизменность (упомянутая в первом абзаце вопроса) не является простой задачей, а также ценные ресурсы, которые объясняют эту тему более подробно. Баллы не измеряют, был ли этот ответ «полезным» для других. Принятый ответ означает, что это был тот ответ, который искал OP, который получил ваш ответ.
Джесси
@Jesse, он не отвечает на вопрос в конце первого предложения, он отвечает на вопрос «Безопасен ли доступ к неизменяемому объекту», который может или не может относиться к вопросу OP, как он указывает в следующем предложении. По сути, это ответ типа "пойди и разберись сам", только для ссылки, что не является хорошим ответом для SO. Что касается голосов «за», я думаю, что это больше связано с возрастом 10,5 лет и темой, которую часто ищут. За последние несколько лет он получил лишь несколько чистых голосов, так что, возможно, люди приходят :).
BeeOnRope
37

Чтения безопасны с точки зрения синхронизации, но не с точки зрения памяти. Это то, что часто неправильно понимают Java-разработчики, в том числе здесь, в Stackoverflow. (Обратите внимание на рейтинг этого ответа для доказательства.)

Если у вас запущены другие потоки, они могут не увидеть обновленную копию HashMap, если в текущем потоке нет записи в память. Запись в память происходит за счет использования ключевых слов synchronized или volatile или некоторых конструкций параллелизма java.

Подробнее см. Статью Брайана Гетца о новой модели памяти Java .

Heath Borders
источник
Извините за двойную отправку, Хит, я заметил вашу только после того, как отправил свою. :)
Александр
2
Я просто рад, что здесь есть другие люди, которые действительно понимают эффекты памяти.
Хит Бордерс,
1
Действительно, хотя ни один поток не увидит объект до того, как он будет правильно инициализирован, я не думаю, что в данном случае это вызывает беспокойство.
Дэйв Л.
1
Это полностью зависит от того, как инициализируется объект.
Билл Мичелл,
1
В вопросе говорится, что после инициализации HashMap он не намерен обновлять ее дальше. С этого момента он просто хочет использовать ее как структуру данных только для чтения. Я думаю, это было бы безопасно, при условии, что данные, хранящиеся на его карте, неизменны.
Бинита Бхарати,
9

Немного посмотрев, я нашел это в java-документе (выделено мной):

Обратите внимание, что эта реализация не синхронизирована. Если несколько потоков обращаются к хэш-карте одновременно, и хотя бы один из потоков структурно модифицирует карту, она должна быть синхронизирована извне. (Структурная модификация - это любая операция, которая добавляет или удаляет одно или несколько сопоставлений; простое изменение значения, связанного с ключом, который уже содержит экземпляр, не является структурной модификацией.)

Похоже, это означает, что это будет безопасно, если верно обратное утверждение.

Дэйв Л.
источник
1
Хотя это отличный совет, как утверждают другие ответы, в случае неизменяемого, безопасно опубликованного экземпляра карты есть более подробный ответ. Но делать это следует только в том случае, если вы знаете, что делаете.
Alex Miller
1
Надеюсь, с такими вопросами больше из нас сможет узнать, что мы делаем.
Дэйв Л.
Это не совсем правильно. Как утверждают другие ответы, между последней модификацией и всеми последующими «поточно-ориентированными» чтениями должно быть событие, которое произошло раньше . Обычно это означает, что вы должны безопасно опубликовать объект после того, как он был создан и внесены изменения. См. Первый отмеченный правильный ответ.
markspace
9

Следует отметить, что при некоторых обстоятельствах get () из несинхронизированного HashMap может вызвать бесконечный цикл. Это может произойти, если одновременный метод put () вызывает повторное хеширование карты.

http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html

Алекс Миллер
источник
1
На самом деле я видел, как это зависание JVM без использования ЦП (что, возможно, хуже)
Питер Лоури,
2
Я думаю , что этот код не был переписан таким образом, что это уже не возможно , чтобы получить бесконечный цикл. Но вы все равно не должны получать и вставлять из несинхронизированной HashMap по другим причинам.
Alex Miller
@AlexMiller, даже если не считать других причин (я полагаю, вы имеете в виду безопасную публикацию), я не думаю, что изменение реализации должно быть причиной для ослабления ограничений доступа, если это явно не разрешено документацией. Так получилось, что документация HashMap Javadoc для Java 8 все еще содержит это предупреждение:Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally.
shmosel
8

Однако есть важный поворот. Доступ к карте безопасен, но в целом не гарантируется, что все потоки будут видеть одно и то же состояние (и, следовательно, значения) HashMap. Это может произойти в многопроцессорных системах, где модификации HashMap, сделанные одним потоком (например, тем, который его заполнил), могут находиться в кэше этого ЦП и не будут видны потокам, запущенным на других ЦП, пока не будет выполнена операция ограничения памяти. выполняется обеспечение согласованности кеша. Спецификация языка Java четко указывает на это: решение состоит в том, чтобы получить блокировку (synchronized (...)), которая вызывает операцию ограничения памяти. Итак, если вы уверены, что после заполнения HashMap каждый из потоков получает ЛЮБУЮ блокировку, то с этого момента можно получить доступ к HashMap из любого потока, пока HashMap не будет снова изменен.

Александр
источник
Я не уверен, что поток, обращающийся к нему, получит какую-либо блокировку, но я уверен, что они не получат ссылку на объект до тех пор, пока он не будет инициализирован, поэтому я не думаю, что у них может быть устаревшая копия.
Дэйв Л.
@Alex: ссылка на HashMap может быть изменчивой для создания тех же гарантий видимости памяти. @ Dave: Это есть возможность увидеть ссылки на новые OBJS до работы его CTOR становится видимой для потока.
Крис Вест,
@Christian В общем случае, конечно. Я говорил, что в этом коде это не так.
Дэйв Л.
Получение СЛУЧАЙНОЙ блокировки не гарантирует, что будет очищен весь кеш процессора потока. Это зависит от реализации JVM, и, скорее всего, это не так.
Пьер
Я согласен с Пьером, не думаю, что достаточно приобрести какой-либо замок. Вы должны выполнить синхронизацию с одним и тем же замком, чтобы изменения стали видимыми.
Константин Милютин
5

Согласно http://www.ibm.com/developerworks/java/library/j-jtp03304/ # Безопасность при инициализации вы можете сделать HashMap последним полем, и после завершения конструктора оно будет безопасно опубликовано.

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

Bodrin
источник
Этот ответ низкого качества, он такой же, как ответ от @taylor gauthier, но с меньшими подробностями.
Snicolas
1
Эмммм ... не быть задницей, но у вас все наоборот. Тейлор сказал: «Нет, посмотрите это сообщение в блоге, ответ может вас удивить», в то время как этот ответ фактически добавляет что-то новое, чего я не знал ... О связи между записью последнего поля в конструктор. Это отличный ответ, и я рад, что прочитал его.
Ajax
А? Это единственный правильный ответ, который я нашел после прокрутки ответов с более высоким рейтингом. Ключ благополучно опубликован, и это единственный ответ, в котором он упоминается.
BeeOnRope
3

Итак, сценарий, который вы описали, заключается в том, что вам нужно поместить кучу данных в карту, а затем, когда вы закончите ее заполнение, вы будете рассматривать ее как неизменяемую. Один из подходов, который является «безопасным» (то есть вы обеспечиваете, чтобы он действительно считался неизменным), заключается в замене ссылки на, Collections.unmodifiableMap(originalMap)когда вы готовы сделать ее неизменной.

Чтобы увидеть, насколько сильно карты могут выходить из строя при одновременном использовании, и предлагаемый обходной путь, о котором я упоминал, посмотрите эту запись о параде ошибок: bug_id = 6423457

Будет
источник
2
Это «безопасно», поскольку обеспечивает неизменность, но не решает проблему безопасности потоков. Если карта безопасна для доступа с оболочкой UnmodifiableMap, то безопасно и без нее, и наоборот.
Дэйв Л.
2

Этот вопрос рассматривается в книге Брайана Гетца «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и его инициализация выполняется в конструкторе класса владельца, любой поток, который позже прочитает эту карту, гарантированно увидит ее на момент завершения работы конструктора, при условии, что никакой другой поток не будет пытаться изменить содержимое карты.

escudero380
источник
1

Имейте в виду, что даже в однопоточном коде замена ConcurrentHashMap на HashMap может быть небезопасной. ConcurrentHashMap запрещает null в качестве ключа или значения. HashMap их не запрещает (не спрашивайте).

Таким образом, в маловероятной ситуации, когда ваш существующий код может добавить нуль в коллекцию во время установки (предположительно в случае какого-либо сбоя), замена коллекции, как описано, изменит функциональное поведение.

Тем не менее, при условии, что вы больше ничего не делаете, одновременное чтение из HashMap безопасно.

[Изменить: под «одновременным чтением» я подразумеваю, что одновременных модификаций также нет.

В других ответах объясняется, как этого добиться. Один из способов - сделать карту неизменной, но это не обязательно. Например, модель памяти JSR133 явно определяет запуск потока как синхронизированное действие, что означает, что изменения, сделанные в потоке A до того, как он запустит поток B, видны в потоке B.

Я не собираюсь противоречить этим более подробным ответам о модели памяти Java. Этот ответ призван указать, что даже помимо проблем с параллелизмом, существует по крайней мере одно различие API между ConcurrentHashMap и HashMap, которое может уничтожить даже однопоточную программу, заменяющую одну на другую.]

Стив Джессоп
источник
Спасибо за предупреждение, но попытки использовать нулевые ключи или значения отсутствуют.
Дэйв Л.
Думал, что не будет. Нули в коллекциях - безумный уголок Java.
Стив Джессоп,
Я не согласен с этим ответом. «Одновременное чтение из HashMap безопасно» само по себе неверно. Он не указывает, происходят ли чтения для карты, которая является изменяемой или неизменной. Чтобы быть правильным, он должен читать: «Одновременные чтения из неизменяемой HashMap безопасны»
Тейлор Готье
2
Не в соответствии со статьями, на которые вы сами ссылались: требование состоит в том, что карту нельзя изменять (и предыдущие изменения должны быть видны всем читателям), а не в том, чтобы она была неизменной (это технический термин в Java и достаточное, но не необходимое условие безопасности).
Стив Джессоп,
Также примечание ... инициализация класса неявно синхронизируется с той же блокировкой (да, вы можете взаимоблокироваться в инициализаторах статических полей), поэтому, если ваша инициализация происходит статически, никто другой не сможет увидеть ее до завершения инициализации, поскольку они должны быть заблокированы в методе ClassLoader.loadClass для той же полученной блокировки ... И если вам интересно, что разные загрузчики классов имеют разные копии одного и того же поля, вы будете правы ... но это будет ортогонально понятие условий гонки; статические поля загрузчика классов разделяют ограждение памяти.
Ajax
0

http://www.docjar.com/html/api/java/util/HashMap.java.html

вот источник для HashMap. Как видите, здесь нет абсолютно никакого кода блокировки / мьютекса.

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

Что интересно, и .NET HashTable, и Dictionary <K, V> имеют встроенный код синхронизации.

FlySwat
источник
2
Я думаю, что есть классы, в которых простое одновременное чтение может вызвать проблемы, например, из-за внутреннего использования временных переменных экземпляра. Так что, вероятно, нужно внимательно изучить исходный код, а не просто быстро просмотреть код блокировки / мьютекса.
Дэйв Л.
0

Если инициализация и каждый ввод синхронизированы, вы сохранены.

Следующий код сохраняется, потому что загрузчик классов позаботится о синхронизации:

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, потому что final также является изменчивым. И если метод является конструктором.

TomWolk
источник