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



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



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

714   11  

11 ответов:

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

  1. обмен ссылками через правильно заблокированное поле (JLS 17.4.5)
  2. используйте статический инициализатор для инициализации магазинов ( JLS 12.4)
  3. обмен ссылками через волатильное поле (JLS 17.4.5), или как следствие этого правила, через классы AtomicX
  4. инициализировать значение в конечном поле ( JLS 17.5).

наиболее интересными для вашего сценария являются (2), (3) и (4). В частности, (3) применяется непосредственно к код у меня выше: если вы преобразуете объявление MAP to:

public static volatile HashMap<Object, Object> MAP;

тогда все кошерно: читатели, которые видят non-null значение должно быть происходит-перед связь с магазином в MAP и, следовательно, увидеть все магазины, связанные с инициализацией карты.

другие методы изменяют семантику вашего метода, так как оба (2) (используя статический инициализатор) и (4) (используя финал) означает, что вы не можете установить MAP динамически во время выполнения. Если ты этого не сделаешь нужно чтобы сделать это, то просто объявить MAP как static final HashMap<> и вам гарантирована безопасная публикация.

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

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

  • вы уже можете создать объект, который будет присвоен в момент объявленияa: просто использовать

Джереми Мэнсон, Бог, когда дело доходит до модели памяти 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, если нет записи в память из текущего потока. Записи в память происходят с помощью синхронизированных или изменчивых ключевых слов, или через использование некоторых конструкций параллелизма java.

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

после немного больше смотреть, я нашел это в документации (выделено мной):

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

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

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

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

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

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

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

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

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

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

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

Что сказал, при условии, что вы делаете ничто другое одновременное чтение из хэш-карты не безопасно.

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

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

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

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

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

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

Что интересно, что и хэш-таблица .NET, и словарь имеют встроенную синхронизацию код.

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

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

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

Comments

    Ничего не найдено.