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

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

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

Ответ 1

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

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

  • Обмен ссылками через правильно заблокированное поле (JLS 17.4.5)
  • Используйте статический инициализатор для создания хранилищ инициализации (JLS 12.4)
  • Обмен ссылкой через поле volatile (JLS 17.4.5) или как следствие этого правила через классы AtomicX
  • Инициализировать значение в конечном поле (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 для статических членов).
  • Вы хотите назначить объект позже, после того, как ссылка уже видна: используйте поле volatile b.

Что это!

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

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

Подробнее о подробностях см. Shipilev или этот FAQ Мэнсон и Гетц.


[1] Прямо цитируя Shipilev.


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

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

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

Ответ 2

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

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

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

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

Ответ 4

После немного большего поиска я нашел это в java doc (внимание мое):

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

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

Ответ 5

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

Ответ 6

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

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

Ответ 7

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

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

Ответ 8

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

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

Ответ 9

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

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

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

[Edit: by "concurrent reads", я имею в виду, что не допускаются одновременные модификации.

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

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

Ответ 10

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

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

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

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

Ответ 11

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

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

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