Есть случай, когда карта будет построена, и после инициализации она больше никогда не будет изменена. Однако к нему можно будет получить доступ (только с помощью get (key)) из нескольких потоков. Насколько безопасно использовать java.util.HashMap таким образом?
(В настоящее время я с удовольствием использую java.util.concurrent.ConcurrentHashMap, и у меня нет никакой потребности в повышении производительности, но мне просто любопытно, подойдет ли простой HashMap. Следовательно, этот вопрос - нет «Какой из них мне следует использовать?», И это не производительность вопрос. Скорее, вопрос: "Будет ли это безопасно?")
@Heath Borders, если экземпляр a был статически инициализирован немодифицируемым HashMap, он должен быть безопасным для одновременного чтения (поскольку другие потоки не могли пропустить обновления, поскольку обновлений не было), верно?
Если он статически инициализирован и никогда не изменялся за пределами статического блока, тогда это может быть нормально, потому что вся статическая инициализация синхронизируется ClassLoader. Это стоит отдельного вопроса. Я бы все равно явно синхронизировал его и профиль, чтобы убедиться, что это вызывает реальные проблемы с производительностью.
@HeathBorders - что вы имеете в виду под "обновлениями памяти"? JVM - это формальная модель, которая определяет такие вещи, как видимость, атомарность, отношения случается раньше, но не использует такие термины, как «обновления памяти». Вы должны уточнить, желательно используя терминологию из JLS.
@Dave - Я полагаю, вы все еще не ищете ответа через 8 лет, но для записи основная путаница почти во всех ответах заключается в том, что они сосредоточены на действия, которые вы выполняете над объектом карты. Вы уже объяснили, что никогда не изменяете объект, так что все это не имеет значения. Единственная потенциальная проблема - это как вы публикуете ссылку для Map, которую вы не объяснили. Если вы не сделаете это безопасно, это будет небезопасно. Если вы сделаете это безопасно, это. Подробности в моем ответе.




http://www.docjar.com/html/api/java/util/HashMap.java.html
вот источник для HashMap. Как видите, здесь нет абсолютно никакого кода блокировки / мьютекса.
Это означает, что, хотя в многопоточной ситуации можно читать из HashMap, я бы определенно использовал ConcurrentHashMap, если бы было несколько записей.
Что интересно, и .NET HashTable, и Dictionary <K, V> имеют встроенный код синхронизации.
Я думаю, что есть классы, в которых простое одновременное чтение может вызвать проблемы, например, из-за внутреннего использования временных переменных экземпляра. Поэтому, вероятно, нужно внимательно изучить исходный код, а не просто быстро просмотреть код блокировки / мьютекса.
Немного посмотрев, я нашел это в java doc (выделено мной):
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. (A structural modification is any operation that adds or deletes one or more mappings; merely changing the value associated with a key that an instance already contains is not a structural modification.)
Похоже, это означает, что это будет безопасно, если верно обратное утверждение.
Хотя это отличный совет, как утверждают другие ответы, в случае неизменяемого, безопасно опубликованного экземпляра карты есть более подробный ответ. Но делать это следует только в том случае, если вы знаете, что делаете.
Надеюсь, с такими вопросами больше из нас сможет узнать, что мы делаем.
Это не совсем правильно. Как утверждают другие ответы, между последней модификацией и всеми последующими «поточно-безопасными» чтениями должен быть случается раньше. Обычно это означает, что вы должны безопасно опубликовать объект после, который был создан и внесены изменения. См. Первый отмеченный правильный ответ.
Имейте в виду, что даже в однопоточном коде замена ConcurrentHashMap на HashMap может быть небезопасной. ConcurrentHashMap запрещает null в качестве ключа или значения. HashMap их не запрещает (не спрашивайте).
Таким образом, в маловероятной ситуации, когда ваш существующий код может добавить ноль в коллекцию во время установки (предположительно, в случае какого-либо сбоя), замена коллекции, как описано, изменит функциональное поведение.
Тем не менее, при условии, что вы больше ничего не делаете, одновременные чтения из HashMap безопасны.
[Edit: под «одновременным чтением» я подразумеваю, что одновременных модификаций также нет.
В других ответах объясняется, как этого добиться. Один из способов - сделать карту неизменной, но это не обязательно. Например, модель памяти JSR133 явно определяет запуск потока как синхронизированное действие, что означает, что изменения, сделанные в потоке A до того, как он запустит поток B, видны в потоке B.
Я не собираюсь противоречить этим более подробным ответам о модели памяти Java. Этот ответ призван указать, что даже помимо проблем с параллелизмом, между ConcurrentHashMap и HashMap есть по крайней мере одно различие в API, которое может уничтожить даже однопоточную программу, заменяющую одну на другую.]
Спасибо за предупреждение, но попыток использовать нулевые ключи или значения нет.
Думал, что не будет. нули в коллекциях - безумный уголок Java.
Я не согласен с этим ответом. «Одновременное чтение из HashMap безопасно» само по себе неверно. Он не указывает, происходит ли чтение для карты, которая является изменяемой или неизменной. Чтобы быть правильным, он должен читать: «Одновременные чтения из неизменяемой HashMap безопасны».
Не в соответствии со статьями, на которые вы сами ссылались: требование состоит в том, что карта не должна изменяться (и предыдущие изменения должны быть видны всем читателям), а не в том, чтобы она была неизменной (это технический термин в Java и достаточное, но не необходимое условие безопасности).
Также примечание ... инициализация класса неявно синхронизируется с той же блокировкой (да, вы можете взаимоблокироваться в инициализаторах статических полей), поэтому, если ваша инициализация происходит статически, никто другой не сможет увидеть ее до завершения инициализации, так как они должны быть заблокированы в методе ClassLoader.loadClass для той же полученной блокировки ... И если вам интересно, что разные загрузчики классов имеют разные копии одного и того же поля, вы были бы правы ... но это было бы ортогонально понятие условий гонки; статические поля загрузчика классов разделяют ограждение памяти.
Джереми Мэнсон, бог, когда дело доходит до модели памяти 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
Это хороший момент, но я полагаюсь на статическую инициализацию, во время которой никакие ссылки не уходят, так что это должно быть безопасно.
Я не понимаю, насколько это высоко оцененный ответ (или даже ответ). Во-первых, это даже не отвечать вопрос и не упоминается один ключевой принцип, который будет определять, безопасно это или нет: безопасная публикация. «Ответ» сводится к «хитрости», и вот три (сложные) ссылки, которые вы можете прочитать.
Он отвечает на вопрос в самом конце первого предложения. Что касается ответа, он поднимает вопрос о том, что неизменность (упомянутая в первом абзаце вопроса) непроста, а также ценные ресурсы, которые объясняют эту тему более подробно. Баллы не измеряют, был ли этот ответ «полезным» для других. Принятый ответ означает, что это был тот ответ, который искал ОП, который получил ваш ответ.
@Jesse, он не отвечает на вопрос в конце первого предложения, он отвечает на вопрос «Безопасен ли доступ к неизменяемому объекту», который может или не может относиться к вопросу OP, как он указывает в следующем предложении. По сути, это ответ типа «пойди и разберись сам» почти только для ссылки, что не является хорошим ответом для SO. Что касается голосов «за», я думаю, что это больше связано с возрастом 10,5 лет и темой, которую часто ищут. За последние несколько лет он получил очень мало голосов, так что, возможно, люди приходят :).
Чтения безопасны с точки зрения синхронизации, но не с точки зрения памяти. Это то, что часто неправильно понимают Java-разработчики, в том числе здесь, в Stackoverflow. (Обратите внимание на рейтинг этот ответ для доказательства.)
Если у вас запущены другие потоки, они могут не увидеть обновленную копию HashMap, если в текущем потоке нет записи в память. Запись в память происходит с использованием ключевых слов synchronized или volatile или с использованием некоторых конструкций параллелизма java.
Подробнее см. Статья Брайана Гетца о новой модели памяти Java.
Извините за двойную отправку, Хит, я заметил вашу только после того, как отправил свою. :)
Я просто рад, что здесь есть другие люди, которые действительно понимают эффекты памяти.
В самом деле, хотя ни один поток не увидит объект до того, как он будет правильно инициализирован, я не думаю, что в данном случае это вызывает беспокойство.
Это полностью зависит от того, как инициализируется объект.
В вопросе говорится, что после инициализации HashMap он больше не намерен обновлять ее. С этого момента он просто хочет использовать ее как структуру данных только для чтения. Я думаю, это было бы безопасно, при условии, что данные, хранящиеся на его карте, неизменны.
На чем вы это основываете? Вы читали ссылки, которые я предоставил в своем ответе?
Чтобы было ясно, в стандарте нет таких терминов, как «запись в память» или подобных вещей. JLS описывает абстрактную модель, которая в основном основана на отношении случается раньше, поэтому утверждение, подобное «Чтения безопасны с точки зрения синхронизации, но не с точки зрения памяти»., не может быть подтверждено (и мне не ясно, что вы под этим подразумеваете). Ключ - безопасная публикация, который гарантирует, что все другие потоки, которые видят ссылку, увидят полностью построенную хэш-карту. Без этого вы небезопасны с точки зрения любой.
Да, я говорю, что вам нужна связь happens-before через изменчивый член или последний член в конструкторе.
Если вы недавно проголосовали против, объясните почему в комментариях. Это просто хорошие манеры. :)
Однако есть важный поворот. Доступ к карте безопасен, но в целом не гарантируется, что все потоки будут видеть одно и то же состояние (и, следовательно, значения) HashMap. Это может произойти в многопроцессорных системах, где модификации HashMap, сделанные одним потоком (например, тем, который его заполнил), могут находиться в кэше этого ЦП и не будут видны потокам, запущенным на других ЦП, пока не будет выполнена операция ограничения памяти выполняется обеспечение согласованности кеша. Спецификация языка Java четко указывает на это: решение состоит в том, чтобы получить блокировку (synchronized (...)), которая вызывает операцию ограничения памяти. Итак, если вы уверены, что после заполнения HashMap каждый из потоков получает ЛЮБУЮ блокировку, то с этого момента можно получить доступ к HashMap из любого потока, пока HashMap не будет снова изменен.
Я не уверен, что поток, обращающийся к нему, получит какую-либо блокировку, но я уверен, что они не получат ссылку на объект до тех пор, пока он не будет инициализирован, поэтому я не думаю, что у них может быть устаревшая копия.
@Alex: ссылка на HashMap может быть изменчивой для создания тех же гарантий видимости памяти. @Dave: является позволяет видеть ссылки на новые объекты до того, как работа его ctor станет видимой для вашего потока.
@Christian В общем случае, конечно. Я говорил, что в этом коде это не так.
Получение СЛУЧАЙНОЙ блокировки не гарантирует полную очистку кэша ЦП потока. Это зависит от реализации JVM, и, скорее всего, это не так.
Я согласен с Пьером, не думаю, что достаточно приобрести какой-либо замок. Вы должны выполнить синхронизацию с одним и тем же замком, чтобы изменения стали видимыми.
Следует отметить, что при некоторых обстоятельствах get () из несинхронизированного HashMap может вызвать бесконечный цикл. Это может произойти, если одновременный метод put () вызывает повторное хеширование карты.
http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html
На самом деле я видел, как это зависание JVM без использования процессора (что, возможно, хуже)
Я считать этот код был переписан так, что больше невозможно получить бесконечный цикл. Но вы все равно не должны получать и вставлять из несинхронизированной HashMap по другим причинам.
@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.
Итак, сценарий, который вы описали, заключается в том, что вам нужно поместить кучу данных в карту, а затем, когда вы закончите ее заполнение, вы будете рассматривать ее как неизменяемую. Один из подходов, который является «безопасным» (то есть вы обеспечиваете, чтобы он действительно считался неизменным), - это заменить ссылку на Collections.unmodifiableMap(originalMap), когда вы будете готовы сделать ее неизменной.
Чтобы увидеть, как сильно карты могут выходить из строя при одновременном использовании, и предлагаемый обходной путь, о котором я упоминал, ознакомьтесь с этой записью парада ошибок: bug_id = 6423457
Это «безопасно», поскольку обеспечивает неизменность, но не решает проблему безопасности потоков. Если карта безопасна для доступа с оболочкой UnmodifiableMap, то безопасно и без нее, и наоборот.
Согласно http://www.ibm.com/developerworks/java/library/j-jtp03304/ # Безопасность инициализации вы можете сделать HashMap последним полем, и после завершения работы конструктора оно будет безопасно опубликовано.
... В новой модели памяти есть что-то похожее на связь «происходит до» между записью последнего поля в конструкторе и начальной загрузкой разделяемой ссылки на этот объект в другом потоке. ...
Этот ответ низкого качества, он такой же, как ответ от @taylor gauthier, но с меньшими подробностями.
Эмммм ... не быть задницей, но у вас все наоборот. Тейлор сказал: «Нет, посмотрите это сообщение в блоге, ответ может вас удивить», тогда как этот ответ на самом деле добавляет что-то новое, о чем я не знал ... конструктор. Это отличный ответ, и я рад, что прочитал его.
Хм? Это правильный ответ Только, который я нашел после прокрутки ответов с более высоким рейтингом. Ключ - безопасно опубликовано, и это единственный ответ, в котором он вообще упоминается.
Если инициализация и каждый ввод синхронизированы, вы экономите.
Следующий код сохраняется, потому что загрузчик классов позаботится о синхронизации:
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 также является изменчивым. А если метод конструктор.
Ваша идиома безопасна если и только если, ссылка на 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]:
Наиболее интересными для вашего сценария являются (2), (3) и (4). В частности, (3) применяется непосредственно к приведенному выше коду: если вы преобразуете объявление MAP в:
public static volatile HashMap<Object, Object> MAP;
тогда все кошерно: читатели, которые видят значение ненулевой, обязательно имеют отношение случается раньше с хранилищем к MAP и, следовательно, видят все хранилища, связанные с инициализацией карты.
Другие методы изменяют семантику вашего метода, поскольку и (2) (с использованием статического инициализатора), и (4) (с использованием окончательный) подразумевают, что вы не можете установить MAP динамически во время выполнения. Если вы не используете необходимость для этого, просто объявите MAP как static final HashMap<>, и вам гарантирована безопасная публикация.
На практике правила просты для безопасного доступа к «неизменяемым объектам»:
Если вы публикуете объект, который не является по своей сути неизменный (как во всех полях, объявленных final) и:
final (включая static final для статических членов).Вот и все!
На практике это очень эффективно. Использование поля static final, например, позволяет JVM предполагать, что значение не меняется в течение всего срока службы программы, и сильно оптимизировать его. Использование поля члена final позволяет архитектурам наиболее читать поле способом, эквивалентным нормальному чтению поля, и не препятствует дальнейшей оптимизации c.
Наконец, использование volatile оказывает определенное влияние: на многих архитектурах (например, x86, особенно на тех, которые не разрешают чтение проходить чтение) не требуется аппаратного барьера, но некоторая оптимизация и переупорядочение могут не происходить во время компиляции, но этот эффект обычно невелик. Взамен вы действительно получаете больше, чем просили - вы можете не только безопасно опубликовать один HashMap, но и сохранить столько немодифицированных HashMap, сколько захотите, в той же ссылке и быть уверенными, что все читатели увидят безопасно опубликованный карта.
Для получения дополнительных сведений см. Шипилев или этот FAQ Мэнсона и Гетца.
[1] Прямая цитата из Шипилев.
a Звучит сложно, но я имею в виду, что вы можете назначить ссылку во время создания - либо в точке объявления, либо в конструкторе (поля-члены), либо в статическом инициализаторе (статические поля).
b При желании вы можете использовать метод synchronized для получения / установки, AtomicReference или что-то в этом роде, но мы говорим о минимальной работе, которую вы можете выполнить.
c В некоторых архитектурах с очень слабыми моделями памяти (я смотрю на ты, Alpha) может потребоваться какой-то барьер чтения перед чтением final, но сегодня это очень редко.
never modifyHashMap не означает, что state of the map object потокобезопасен, я думаю. Бог знает реализацию библиотеки, если в официальном документе не сказано, что она потокобезопасна.
@JiangYD - вы правы, в некоторых случаях там есть серая зона: когда мы говорим «изменить», мы на самом деле имеем в виду любое действие, которое внутренне выполняет некоторый пишет, который может конкурировать с чтением или записью в других потоках. Эти записи могут быть внутренними деталями реализации, поэтому даже операция, которая кажется «только для чтения», такая как get(), на самом деле может выполнять некоторые записи, скажем, обновляя некоторую статистику (или, в случае упорядоченного доступа LinkedHashMap, обновляет порядок доступа). Итак, хорошо написанный класс должен предоставить некоторую документацию, которая проясняет, если ...
... очевидно, что операции "только для чтения" на самом деле доступны только для чтения изнутри с точки зрения безопасности потоков. В стандартной библиотеке C++, например, существует общее правило, согласно которому функция-член, помеченная const, действительно доступна только для чтения в таком смысле (внутри они все еще могут выполнять записи, но их нужно сделать потокобезопасными). В Java нет ключевого слова const, и мне не известно о какой-либо задокументированной полной гарантии, но в целом классы стандартной библиотеки ведут себя так, как ожидалось, а исключения задокументированы (см. Пример LinkedHashMap, где операции RO, такие как get, явно упоминаются как небезопасные).
@JiangYD - наконец, возвращаясь к вашему исходному вопросу, для HashMap у нас действительно есть прямо в документации, поточно-безопасное поведение для этого класса: Если несколько потоков обращаются к хэш-карте одновременно, и хотя бы один из потоков структурно модифицирует карту, она должна быть синхронизирована извне. (Структурная модификация - это любая операция, которая добавляет или удаляет одно или несколько сопоставлений; простое изменение значения, связанного с ключом, который уже содержит экземпляр, не является структурной модификацией.)
Таким образом, методы HashMap, которые, как мы ожидаем, будут доступны только для чтения, доступны только для чтения, поскольку они не изменяют структуру HashMap. Конечно, эта гарантия может не действовать для других произвольных реализаций Map, но речь идет именно о HashMap.
@ BeeOnRope - Сделай сцены
@BeeOnRope Нужно ли каким-то образом синхронизировать конструкцию HashMap перед вызовом SomeClass.setMap в теперь изменчивое поле SomeClass.MAP, чтобы гарантировать отсутствие переупорядочения инструкций в отношении сохранения вновь созданного HashMap на volatileSomeClass.MAP? Эта статья предполагает, что необходимо синхронизировать «put» HashMap, чтобы гарантировать, что другие потоки видят внутреннее устройство карты в согласованном состоянии, в дополнение к использованию volatile.
@Jose - нет, потому что запись в изменчивое поле синхронизируется с последующим чтением. Все действия перед записью в один и тот же поток происходят - до записи и чтения - до того, как все будут использованы в потребляющем потоке, поэтому все действия построения будут упорядочены до использования доступами к изменчивому полю.
Этот вопрос рассматривается в книге Брайана Гетца «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, и его инициализация выполняется в конструкторе класса владельца, любой поток, который позже прочитает эту карту, гарантированно увидит ее на момент завершения конструктора, при условии, что никакой другой поток не будет пытаться изменить содержимое карты. .
Многие ответы здесь верны в отношении взаимного исключения из запущенных потоков, но неверны в отношении обновлений памяти. Я проголосовал за / против соответственно, но все еще есть много неправильных ответов с положительными голосами.