Чи безпечно отримувати значення з java.util.HashMap з декількох потоків (без змін)?


138

Існує випадок, коли карта буде побудована, і як тільки вона буде ініціалізована, вона більше ніколи не буде змінена. Однак доступ до нього (лише через get (key)) можна отримати з декількох потоків. Чи безпечно використовувати java.util.HashMapтакий спосіб?

(В даний час я із задоволенням використовую програму java.util.concurrent.ConcurrentHashMapта не маю міри необхідності в підвищенні продуктивності, але мені просто цікаво, чи HashMapвистачить простого . Отже, це питання не "Кого я повинен використовувати?", А також не питання про продуктивність. Швидше питання "Чи було б це безпечно?")


4
Багато відповідей тут правильні щодо взаємного виключення із запущених потоків, але неправильні щодо оновлення пам'яті. Я відповідно проголосував «за» / «вниз», але є ще багато неправильних відповідей з позитивними голосами.
Гірські кордони

@Heath Borders, якщо екземпляр a був статично ініціалізований немодифікованим HashMap, він повинен бути безпечним для одночасного зчитування (оскільки інші потоки не могли пропустити оновлення, оскільки не було оновлень), правда?
kaqqao

Якщо вона статично ініціалізована і ніколи не змінюється за межами статичного блоку, то це може бути нормально, оскільки вся статична ініціалізація синхронізується символом ClassLoader. Це варто окремо запитати самостійно. Я все одно явно синхронізував би його та профіль, щоб переконатися, що це викликає реальні проблеми з продуктивністю.
Хіт Межі

@HeathBorders - що ви розумієте під "оновленнями пам'яті"? JVM - це формальна модель, яка визначає такі речі, як видимість, атомність, відносини, що відбуваються раніше , але не використовує такі терміни, як "оновлення пам'яті". Вам слід уточнити, бажано, використовуючи термінологію з JLS.
BeeOnRope

2
@Dave - я припускаю, що ви все ще не шукаєте відповіді через 8 років, але для запису, ключова плутанина майже у всіх відповідях полягає в тому, що вони зосереджені на діях, які ви робите над об’єктом карти . Ви вже пояснили, що ви ніколи не змінюєте об'єкт, тому це абсолютно не має значення. Єдиний потенційний "gotcha" - це те, як ви публікуєте посилання на те Map, чого ви не пояснили. Якщо ви не зробите це безпечно, це не безпечно. Якщо ти це робиш благополучно, так і є . Деталі у моїй відповіді.
BeeOnRope

Відповіді:


55

Ваша ідіома безпечна тоді і тільки тоді , коли посилання на 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для доступу до карти, і перевіряють наявність нуля, як це:

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] Безпосередньо цитуючи з shipilev .


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

b Необов'язково, ви можете використовувати synchronizedметод для отримання / встановлення, AtomicReferenceабо щось або щось, але ми говоримо про мінімальну роботу, яку ви можете виконати.

c Деякі архітектури з дуже слабкими моделями пам'яті (я дивлюся на вас , Альфа), можливо, потребують певного бар'єру читання перед finalпрочитаним - але це сьогодні дуже рідко.


never modify HashMapне означає, що state of the map objectце безпечно для потоків, я думаю. Бог знає реалізацію бібліотеки, якщо в офіційному документі не сказано, що це безпечно для потоків.
Цзян ЙД

@JiangYD - ти маєш рацію, там є сіра зона в деяких випадках: коли ми кажемо «змінити» те, що ми насправді маємо на увазі, це будь-яка дія, яка внутрішньо виконує деякі записи, які можуть змагатись з читанням або записом в інших потоках. Ці записи можуть бути внутрішніми деталями реалізації, тому навіть операція, яка, здається, "лише для читання", схожа, get()може фактично виконати деякі записи, скажімо, оновлення деяких статистичних даних (або у випадку LinkedHashMapоновлення порядку замовлення доступу впорядкованому доступом). Тож добре написаний клас повинен надати деяку документацію, яка дає зрозуміти, якщо ...
BeeOnRope

... мабуть, операції "лише для читання" дійсно є внутрішніми лише для читання у сенсі безпеки потоку. Наприклад, у стандартній бібліотеці C ++ існує правило, що позначені функції члена constсправді доступні лише для читання в такому розумінні (внутрішньо вони все ще можуть виконувати записи, але їх потрібно зробити безпечними для потоків). У constJava немає ключового слова, і я не знаю жодної документально підтвердженої гарантії, але загалом класичні бібліотечні класи поводяться так, як очікувалося, і винятки задокументовані (див. LinkedHashMapПриклад, коли RO ops як getявно згадується як небезпечний).
BeeOnRope

@JiangYD - нарешті, повертаючись до вашого початкового питання, оскільки HashMapми фактично маємо право в документації поведінки щодо безпеки потоку для цього класу: Якщо кілька потоків одночасно отримують доступ до хеш-карти, і принаймні один з потоків структурно модифікує карту, вона повинна бути синхронізована зовні. (Структурна модифікація - це будь-яка операція, яка додає або видаляє одне або кілька відображень; просто зміна значення, пов’язаного з ключем, який вже містить екземпляр, не є структурною модифікацією.)
BeeOnRope

Тож HashMapметоди, які ми очікуємо лише для читання, є лише для читання, оскільки вони структурно не змінюють HashMap. Звичайно, ця гарантія може не застосовуватися для довільних інших Mapреалізацій, але питання стосується HashMapконкретно.
BeeOnRope

70

Джеремі Менсон, бог, що стосується Моделі пам’яті 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
Це хороший момент, але я покладаюся на статичну ініціалізацію, під час якої жодних посилань не залишається, тому це повинно бути безпечним.
Дейв Л.

5
Я не бачу, як це висока оцінка (чи навіть відповідь). Він, наприклад, навіть не відповідає на це запитання, і не згадує про один ключовий принцип, який вирішить, безпечний він чи ні: безпечне публікація . "Відповідь" зводиться до "це хитро", і ось три (складні) посилання, які ви можете прочитати.
BeeOnRope

Він відповідає на питання в самому кінці першого речення. Що стосується відповіді, він наголошує на тому, що незмінність (про яку йдеться в першому пункті запитання) не є однозначною, поряд із цінними ресурсами, що пояснюють цю тему далі. Бали не вимірюють, чи це відповідь, а вимірюють, чи відповідь була "корисною" для інших. Прийняття відповіді означає, що саме відповідь шукала, яку отримала ваша відповідь.
Джессі

@Jesse він не відповідає на питання в кінці першого речення, він відповідає на питання "чи безпечно отримати доступ до незмінного об'єкта", що може, а може і не стосуватися питання ОП, як він вказує в наступному реченні. По суті, це майже відповідь типу "лише зрозумій сам" відповідь, що не є гарною відповіддю для ТА. Щодо оновлених результатів, я думаю, що це більше функція - 10,5 років і тема, яку часто шукають. За останні кілька років було отримано дуже мало чистих оновлень, тому, можливо, люди йдуть :).
BeeOnRope

35

Показання є безпечними з точки зору синхронізації, але не з точки зору пам'яті. Це те, що широко не зрозуміло серед розробників Java, включаючи тут, на Stackoverflow. (Дотримуйтесь оцінки цієї відповіді для підтвердження.)

Якщо у вас запущені інші потоки, вони можуть не побачити оновлену копію HashMap, якщо у поточному потоці немає запису пам'яті. Запис у пам'ять відбувається через використання синхронізованих або мінливих ключових слів або через використання деяких конструкцій Java-одночасності.

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


Вибачте за подвійне подання Хіт, я помітив ваше лише після того, як я подав свою. :)
Олександр

2
Я просто радий, що тут є інші люди, які насправді розуміють ефекти пам’яті.
Гейт-Межі

1
Дійсно, хоча жодна нитка не побачить об’єкт до його ініціалізації належним чином, тому я не думаю, що це викликає занепокоєння в цьому випадку.
Дейв Л.

1
Це повністю залежить від ініціалізації об'єкта.
Білл Мішелл

1
Питання говорить про те, що після ініціалізації HashMap він не має наміру більше його оновлювати. Відтоді він просто хоче використовувати його як структуру даних лише для читання. Я думаю, це було б безпечно зробити за умови, що дані, що зберігаються на його карті, є незмінними.
Бініта Бхараті

9

Трохи роздивившись, я виявив це в документі java doc (акцент мій):

Зауважте, що ця реалізація не синхронізована. Якщо декілька потоків отримують доступ до хеш-карти одночасно, і принаймні один з потоків модифікує карту структурно, вона повинна бути синхронізована зовні. (Структурна модифікація - це будь-яка операція, яка додає або видаляє одне або більше відображень; просто зміна значення, пов'язаного з ключем, який вже містить екземпляр, не є структурною модифікацією.)

Це, мабуть, означає, що це буде безпечно, якщо припустити, що зворотне твердження є правдивим.


1
Хоча це відмінна порада, як зазначають інші відповіді, у випадку незмінного безпечно опублікованого екземпляра карти є більш нюансований відповідь. Але ти повинен робити це лише у тому випадку, якщо ти знаєш, що ти робиш.
Алекс Міллер

1
Будемо сподіватися, що такі питання з нас можуть знати, що ми робимо.
Дейв Л.

Це не зовсім правильно. Як зазначено в інших відповідях, між останньою модифікацією та всіма наступними зчитуваннями "безпечного потоку" має відбуватися раніше . Зазвичай це означає, що ви повинні безпечно публікувати об’єкт після його створення та внесення його змін. Дивіться першу, позначену правильну відповідь.
Markpace

9

Одне зауваження полягає в тому, що за деяких обставин отримання () від несинхронізованого HashMap може спричинити нескінченний цикл. Це може статися, якщо паралельний put () спричинить повторний перегляд карти.

http://lightbody.net/blog/2005/07/hashmapget_can_ why_an_infini.html


1
Насправді я бачив, як це звисає JVM, не споживаючи процесор (що, можливо, гірше)
Пітер Лоурі

2
Я думаю, що цей код був переписаний таким чином, що неможливо отримати нескінченний цикл. Але ви все одно не повинні намагатися отримувати та передавати з несинхронізованого HashMap з інших причин.
Алекс Міллер

@AlexMiller навіть окрім інших причин (я припускаю, що ви маєте на увазі безпечну публікацію), я не думаю, що зміна впровадження повинна бути причиною для послаблення обмежень доступу, якщо це прямо не передбачено документацією. Як це трапляється, HashMap Javadoc для Java 8 все ще містить це попередження: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.
shmosel

8

Однак є важливий поворот. Доступ до карти безпечно, але в цілому не гарантується, що всі потоки побачать точно той самий стан (і, таким чином, значення) HashMap. Це може статися в багатопроцесорних системах, де модифікації HashMap, здійснені одним потоком (наприклад, той, який його заселив), можуть сидіти в кеш-пам'яті цього процесора і не будуть помічені потоками, що працюють на інших процесорах, поки операція забору пам’яті не буде виконується забезпечення узгодженості кешу. Специфікація мови Java є чіткою для цього: рішення полягає у придбанні блокування (синхронізованого (...)), яке випромінює операцію забору пам'яті. Отже, якщо ви впевнені, що після заповнення HashMap кожен з потоків отримує БУДЬ-який замок, то з цього моменту буде нормально отримати доступ до HashMap з будь-якого потоку, поки HashMap знову не буде змінено.


Я не впевнений, що доступ до цього потоку придбає будь-який замок, але я впевнений, що вони не отримають посилання на об'єкт доти, доки він не буде ініціалізований, тому я не думаю, що у них може бути застаріла копія.
Дейв Л.

@Alex: Посилання на HashMap може бути мінливим, щоб створити ті ж гарантії видимості пам’яті. @ Dave: Це є можливість побачити посилання на нові OBJS до роботи його CTOR стає видимою для потоку.
Кріс Вест

@Christian У загальному випадку, звичайно. Я говорив, що в цьому коді це не так.
Дейв Л.

Придбання блокування RANDOM не гарантує очищення кешу всього потоку процесора. Це залежить від впровадження СВМ, і це, швидше за все, не робиться таким чином.
П’єр

Я погоджуюся з П’єром, я не думаю, що придбання будь-якого замка буде достатньо. Вам потрібно синхронізувати на одному блоку, щоб зміни стали видимими.
damluar

5

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

... У новій моделі пам'яті є щось подібне до відносин, що трапляються раніше, між записом остаточного поля в конструкторі та початковим завантаженням спільної посилання на цей об'єкт в іншій потоці. ...


Ця відповідь низької якості, вона така ж, як відповідь від @taylor gauthier, але з меншою кількістю деталей.
Snicolas

1
Умммм ... не бути дупою, але ти маєш її назад. Тейлор сказав: «ні, подивіться на цю публікацію в блозі, відповідь може вас здивувати», тоді як ця відповідь насправді додає щось нове, чого я не знав ... Про те, що трапляється раніше, ніж стосунки написання остаточного поля в конструктор. Ця відповідь відмінна, і я радий, що я її прочитав.
Аякс

Так? Це єдина правильна відповідь, яку я знайшов після прокручування відповідей з більш високою оцінкою. Ключ надійно публікується, і це єдина відповідь, яка його навіть згадує.
BeeOnRope

3

Отже, описаний вами сценарій полягає в тому, що вам потрібно вставити купу даних на карту, тоді, коли ви закінчите їх, ви вважаєте це незмінним. Один із підходів, який є "безпечним" (тобто ви дотримуєтесь того, що це дійсно трактується як непорушний), - це замінити посилання, Collections.unmodifiableMap(originalMap)коли ви готові зробити його непорушним.

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


2
Це "безпечно", оскільки воно примушує до незмінності, але не стосується проблеми безпеки потоку. Якщо карта безпечна для доступу з обгорткою UnmodifiableMap, вона без неї безпечна, і навпаки.
Дейв Л.

2

Це питання вирішено в книзі Брайана Геца "Конкурс Java в практиці" (Лістинг 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і його ініціалізація здійснюється в конструкторі класу власника, будь-який потік, який згодом прочитає цю карту, гарантовано побачить її з моменту закінчення конструктора, за умови, що жоден інший потік не спробує змінити вміст карти.


1

Попереджуйте, що навіть у однопотоковому коді заміна ConcurrentHashMap на HashMap може не бути безпечною. ConcurrentHashMap забороняє нуль як ключ або значення. HashMap не забороняє їх (не питайте).

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

Однак, за умови, що ви нічого не робите, паралельні читання з HashMap є безпечними.

[Редагувати: під "одночасними читаннями", я маю на увазі, що не існує також одночасних модифікацій.

Інші відповіді пояснюють, як це забезпечити. Один із способів - зробити карту непорушною, але це не обов'язково. Наприклад, модель пам'яті JSR133 чітко визначає, що започаткування потоку має бути синхронізованою дією, тобто зміни, внесені в потік А перед початком потоку B, видно в потоці B.

Моя мета - не суперечити тим більш детальним відповідям щодо моделі пам'яті Java. Ця відповідь призначена для того, щоб вказати, що навіть окрім проблем із паралельністю, між ConcurrentHashMap та HashMap існує, щонайменше, одна різниця API, яка могла б скапувати навіть однопоточну програму, яка замінила одну на іншу.]


Дякуємо за попередження, але немає спроб використовувати нульові ключі чи значення.
Дейв Л.

Думав, цього не буде. нулі в колекціях - шалений куточок Яви.
Стів Джессоп

Я не згоден з цією відповіддю. "Одночасне зчитування з HashMap є безпечним" саме по собі є неправильним. У ній не зазначено, чи відбуваються читання проти карти, яка є змінною чи незмінною. Для коректності слід прочитати "Одночасні читання з непорушного HashMap є безпечними"
Тейлор Готьє,

2
Не відповідно до статей, до яких ви пов’язані самі: вимога полягає в тому, що карта не повинна бути змінена (і попередні зміни повинні бути видимими для всіх читацьких потоків), не те, щоб вона була непорушною (що є технічним терміном на Java і є достатня, але не необхідна умова безпеки).
Стів Джессоп

Також примітка ... Ініціалізація класу неявно синхронізується на тому самому блокуванні (так, ви можете зайти в тупик в ініціалізаторах статичного поля), тому, якщо ваша ініціалізація стане статично, ніхто інший буде неможливо побачити її до завершення ініціалізації, як їх потрібно було б заблокувати методом ClassLoader.loadClass на тому ж придбаному блокуванні ... І якщо вам цікаво, що різні завантажувачі класів мають різні копії одного і того ж поля, ви будете вірні ... але це буде ортогонально поняття перегонових умов; статичні поля завантажувача класів поділяють огорожу пам'яті.
Аякс

0

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

ось джерело для HashMap. Як ви можете сказати, в ньому абсолютно немає коду блокування / mutex.

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

Що цікаво, це те, що і .NET HashTable, і словник <K, V> вбудували код синхронізації.


2
Я думаю, що існують заняття, де просто читання одночасно може спричинити вам проблеми, наприклад, через внутрішнє використання змінних тимчасових примірників. Тому, напевно, потрібно уважно вивчити джерело, ніж швидке сканування для блокування / коду mutex.
Дейв Л.

0

Якщо ініціалізація та кожне введення синхронізовано, ви економите.

Наступний код зберігається, оскільки завантажувач класів подбає про синхронізацію:

public static final HashMap<String, String> map = new HashMap<>();
static {
  map.put("A","A");

}

Наступний код є збереженням, тому що написання летких речовин буде піклуватися про синхронізацію.

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;
  }
}

Це також буде працювати, якщо змінна-член остаточна, оскільки остаточна також мінлива. І якщо метод - конструктор.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.