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

StackOverflow https://stackoverflow.com/questions/104184

Вопрос

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

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

Это было полезно?

Решение

Ваша идиома безопасна тогда и только тогда , когда ссылка на HashMap является безопасно опубликованный.Скорее, чем что-либо, связывающее внутренности HashMap сам по себе, безопасная публикация имеет дело с тем, как поток построения делает ссылку на карту видимой для других потоков.

По сути, единственно возможная гонка здесь - между строительством HashMap и любые потоки чтения, которые могут получить к нему доступ до того, как он будет полностью создан.Большая часть обсуждения посвящена тому, что происходит с состоянием объекта map, но это не имеет значения, поскольку вы никогда не изменяете его, поэтому единственная интересная часть заключается в том, как HashMap ссылка опубликована.

Например, представьте, что вы публикуете карту следующим образом:

class SomeClass {
   public static HashMap<Object, Object> MAP;

   public synchronized static setMap(HashMap<Object, Object> m) {
     MAP = m;
   }
}

...и в какой - то момент setMap() вызывается с помощью map, а другие потоки используют 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) (с использованием Финал) подразумевают, что вы не можете установить MAP динамически во время выполнения.Если ты этого не сделаешь потребность чтобы сделать это, просто объявите MAP в качестве static final HashMap<> и вам гарантируется безопасная публикация.

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

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

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

Вот и все!

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

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

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


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


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

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

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

Другие советы

У Джереми Мэнсона, бога, когда дело доходит до модели памяти Java, есть блог из трех частей на эту тему - потому что, по сути, вы задаете вопрос "Безопасно ли получать доступ к неизменяемой хэш-карте" - ответ на это - да.Но вы должны ответить на предикат на этот вопрос, который звучит так: "Является ли моя хэш-карта неизменяемой".Ответ может вас удивить - 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() из несинхронизированной хэш-карты может вызвать бесконечный цикл.Это может произойти, если параллельная функция put() вызывает перефразирование карты.

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

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

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

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

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

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

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

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

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

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

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

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

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

вот исходный код для HashMap.Как вы можете судить, там абсолютно нет кода блокировки / мьютекса.

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

Что интересно, так это то, что и .СЕТЕВАЯ хэш-таблица, и словарь<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 также изменчива.И если метод является конструктором.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top