Ваша ідіома безпечна тоді і тільки тоді , коли посилання на 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] :
- Обмінятись посиланням через правильно заблоковане поле ( JLS 17.4.5 )
- Використовуйте статичний ініціалізатор, щоб зробити ініціалізаційні сховища ( JLS 12.4 )
- Обміняти посилання за допомогою летючого поля ( JLS 17.4.5 ) або, як наслідок цього правила, через класи AtomicX
- Ініціалізуйте значення в остаточному полі ( 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
прочитаним - але це сьогодні дуже рідко.