Есть случай, когда карта будет построена, и после инициализации она больше никогда не будет изменена. Однако к нему можно будет получить доступ (только с помощью get (key)) из нескольких потоков. Насколько безопасно использовать java.util.HashMap
такой способ?
(В настоящее время я с удовольствием использую a java.util.concurrent.ConcurrentHashMap
, и у меня нет никакой потребности в повышении производительности, но мне просто любопытно, достаточно ли простого HashMap
. Следовательно, этот вопрос не «Какой из них мне использовать?», И это не вопрос производительности. Скорее вопрос: «Будет ли это безопасно?»)
java
multithreading
concurrency
hashmap
Дэйв Л.
источник
источник
ClassLoader
. Это стоит отдельного вопроса. Я бы все равно явно синхронизировал его и профиль, чтобы убедиться, что это вызывает реальные проблемы с производительностью.Map
, которую вы не объяснили. Если вы не сделаете это безопасно, это будет небезопасно. Если вы сделаете это безопасно, то так и будет . Подробности в моем ответе.Ответы:
Ваша идиома безопасна тогда и только тогда , когда ссылка на
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) (с использованием final ) подразумевают, что вы не можете устанавливать
MAP
динамически во время выполнения. Если тебе не нужно , просто объявитеMAP
как a,static final HashMap<>
и безопасная публикация вам гарантирована.На практике правила просты для безопасного доступа к «неизменяемым объектам»:
Если вы публикуете объект, который не является неизменяемым по своей сути (как во всех объявленных полях
final
) и:final
поле (в том числеstatic final
для статических членов).Это оно!
На практике это очень эффективно. Использование
static final
поля, например, позволяет JVM предполагать, что значение не меняется в течение всего срока службы программы, и сильно оптимизировать его. Использование поля-final
члена позволяет большинству архитектур читать поле способом, эквивалентным нормальному чтению поля, и не препятствует дальнейшей оптимизации. c .Наконец, использование
volatile
действительно имеет некоторое влияние: аппаратный барьер не требуется на многих архитектурах (например, x86, особенно на тех, которые не разрешают чтение для передачи чтения), но некоторая оптимизация и переупорядочение могут не происходить во время компиляции - но это эффект вообще небольшой. Взамен вы действительно получаете больше, чем просили - вы можете не только безопасно опубликовать картуHashMap
, но и сохранить столько немодифицированныхHashMap
файлов, сколько захотите, в той же ссылке и быть уверенными, что все читатели увидят безопасно опубликованную карту. .Для получения дополнительных сведений см. Шипилев или этот FAQ Мэнсона и Гетца .
[1] Прямая цитата из Шипилева .
а Это звучит сложно, но я имею в виду, что вы можете назначить ссылку во время создания - либо в точке объявления, либо в конструкторе (поля-члены), либо в статическом инициализаторе (статические поля).
b При желании вы можете использовать
synchronized
метод для получения / установки илиAtomicReference
или что-то еще, но мы говорим о минимальной работе, которую вы можете выполнить.c В некоторых архитектурах с очень слабыми моделями памяти (я смотрю на вас , Alpha) может потребоваться какой-то тип барьера чтения перед
final
чтением, но сегодня это очень редко.источник
never modify
HashMap
state of the map object
Я не думаю, что это потокобезопасный. Бог знает реализацию библиотеки, если в официальном документе не сказано, что она потокобезопасна.get()
может на самом деле выполнять некоторые записи, например, обновление некоторой статистики (или в случаеLinkedHashMap
обновления порядка доступа по порядку доступа). Так что хорошо написанный класс должен содержать некоторую документацию, которая проясняет, если ...const
действительно доступны только для чтения в этом смысле (внутри они все еще могут выполнять записи, но их необходимо сделать потокобезопасными). Вconst
Java нет ключевого слова, и мне не известно о каких-либо задокументированных общих гарантиях, но в целом классы стандартной библиотеки ведут себя так, как ожидалось, а исключения задокументированы (см.LinkedHashMap
Пример, в котором операции ROget
явно упоминаются как небезопасные).HashMap
у нас действительно есть право в документации о поведении безопасности потоков для этого класса: если несколько потоков одновременно обращаются к хэш-карте, и по крайней мере один из потоков структурно изменяет карту, он должен быть синхронизирован внешне. (Структурная модификация - это любая операция, которая добавляет или удаляет одно или несколько сопоставлений; простое изменение значения, связанного с ключом, который уже содержится в экземпляре, не является структурной модификацией.)HashMap
методы, которые, как мы предполагаем, будут доступны только для чтения, доступны только для чтения, поскольку они не изменяют структуруHashMap
. Конечно, эта гарантия может неMap
действовать для произвольных других реализаций, но вопросHashMap
конкретно в этом.Джереми Мэнсон, бог, когда дело доходит до модели памяти 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-разработчики, в том числе здесь, в Stackoverflow. (Обратите внимание на рейтинг этого ответа для доказательства.)
Если у вас запущены другие потоки, они могут не увидеть обновленную копию HashMap, если в текущем потоке нет записи в память. Запись в память происходит за счет использования ключевых слов synchronized или volatile или некоторых конструкций параллелизма java.
Подробнее см. Статью Брайана Гетца о новой модели памяти Java .
источник
Немного посмотрев, я нашел это в java-документе (выделено мной):
Похоже, это означает, что это будет безопасно, если верно обратное утверждение.
источник
Следует отметить, что при некоторых обстоятельствах get () из несинхронизированного HashMap может вызвать бесконечный цикл. Это может произойти, если одновременный метод put () вызывает повторное хеширование карты.
http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html
источник
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.
Однако есть важный поворот. Доступ к карте безопасен, но в целом не гарантируется, что все потоки будут видеть одно и то же состояние (и, следовательно, значения) HashMap. Это может произойти в многопроцессорных системах, где модификации HashMap, сделанные одним потоком (например, тем, который его заполнил), могут находиться в кэше этого ЦП и не будут видны потокам, запущенным на других ЦП, пока не будет выполнена операция ограничения памяти. выполняется обеспечение согласованности кеша. Спецификация языка Java четко указывает на это: решение состоит в том, чтобы получить блокировку (synchronized (...)), которая вызывает операцию ограничения памяти. Итак, если вы уверены, что после заполнения HashMap каждый из потоков получает ЛЮБУЮ блокировку, то с этого момента можно получить доступ к HashMap из любого потока, пока HashMap не будет снова изменен.
источник
Согласно http://www.ibm.com/developerworks/java/library/j-jtp03304/ # Безопасность при инициализации вы можете сделать HashMap последним полем, и после завершения конструктора оно будет безопасно опубликовано.
... В новой модели памяти есть что-то похожее на связь "происходит до" между записью последнего поля в конструкторе и начальной загрузкой разделяемой ссылки на этот объект в другом потоке. ...
источник
Итак, сценарий, который вы описали, заключается в том, что вам нужно поместить кучу данных в карту, а затем, когда вы закончите ее заполнение, вы будете рассматривать ее как неизменяемую. Один из подходов, который является «безопасным» (то есть вы обеспечиваете, чтобы он действительно считался неизменным), заключается в замене ссылки на,
Collections.unmodifiableMap(originalMap)
когда вы готовы сделать ее неизменной.Чтобы увидеть, насколько сильно карты могут выходить из строя при одновременном использовании, и предлагаемый обходной путь, о котором я упоминал, посмотрите эту запись о параде ошибок: bug_id = 6423457
источник
Этот вопрос рассматривается в книге Брайана Гетца «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
и его инициализация выполняется в конструкторе класса владельца, любой поток, который позже прочитает эту карту, гарантированно увидит ее на момент завершения работы конструктора, при условии, что никакой другой поток не будет пытаться изменить содержимое карты.источник
Имейте в виду, что даже в однопоточном коде замена ConcurrentHashMap на HashMap может быть небезопасной. ConcurrentHashMap запрещает null в качестве ключа или значения. HashMap их не запрещает (не спрашивайте).
Таким образом, в маловероятной ситуации, когда ваш существующий код может добавить нуль в коллекцию во время установки (предположительно в случае какого-либо сбоя), замена коллекции, как описано, изменит функциональное поведение.
Тем не менее, при условии, что вы больше ничего не делаете, одновременное чтение из HashMap безопасно.
[Изменить: под «одновременным чтением» я подразумеваю, что одновременных модификаций также нет.
В других ответах объясняется, как этого добиться. Один из способов - сделать карту неизменной, но это не обязательно. Например, модель памяти JSR133 явно определяет запуск потока как синхронизированное действие, что означает, что изменения, сделанные в потоке A до того, как он запустит поток B, видны в потоке B.
Я не собираюсь противоречить этим более подробным ответам о модели памяти Java. Этот ответ призван указать, что даже помимо проблем с параллелизмом, существует по крайней мере одно различие API между ConcurrentHashMap и HashMap, которое может уничтожить даже однопоточную программу, заменяющую одну на другую.]
источник
http://www.docjar.com/html/api/java/util/HashMap.java.html
вот источник для HashMap. Как видите, здесь нет абсолютно никакого кода блокировки / мьютекса.
Это означает, что, хотя в многопоточной ситуации можно читать из HashMap, я бы определенно использовал ConcurrentHashMap, если бы было несколько записей.
Что интересно, и .NET HashTable, и Dictionary <K, V> имеют встроенный код синхронизации.
источник
Если инициализация и каждый ввод синхронизированы, вы сохранены.
Следующий код сохраняется, потому что загрузчик классов позаботится о синхронизации:
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 также является изменчивым. И если метод является конструктором.
источник