Безопасно ли получать значения из java.util.HashMap из нескольких потоков (без изменений)? - PullRequest
126 голосов
/ 19 сентября 2008

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

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

Ответы [ 11 ]

70 голосов
/ 19 сентября 2008

Джереми Мэнсон, бог, когда дело доходит до модели памяти 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

38 голосов
/ 02 февраля 2017

Ваша идиома безопасна тогда и только тогда, когда ссылка на 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] :

  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 как static final HashMap<> и вам гарантированно безопасная публикация.

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

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

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

Вот и все!

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

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

Более подробная информация приведена в Шипилев или в этом FAQ Мэнсона и Гетца .


[1] Прямое цитирование shipilev .


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

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

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

35 голосов
/ 19 сентября 2008

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

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

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

9 голосов
/ 20 сентября 2008

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

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

9 голосов
/ 19 сентября 2008

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

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

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

8 голосов
/ 19 сентября 2008

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

5 голосов
/ 30 августа 2012

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

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

3 голосов
/ 09 ноября 2009

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

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

1 голос
/ 19 сентября 2008

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

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

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

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

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

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

0 голосов
/ 28 сентября 2016

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

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

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. И если метод является конструктором.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...