Важливо розуміти, що для безпеки потоку є два аспекти.
- контроль виконання та
- видимість пам'яті
Перший пов'язаний з контролем, коли виконується код (включаючи порядок виконання інструкцій) та чи може він виконуватись одночасно, а другий - коли ефекти в пам'яті того, що зроблено, видно для інших потоків. Оскільки кожен процесор має кілька рівнів кешу між ним і основною пам'яттю, потоки, що працюють на різних процесорах або ядрах, можуть бачити "пам'ять" по-різному в будь-який момент часу, тому що потокам дозволено отримувати та працювати в приватних копіях основної пам'яті.
Використання synchronized
перешкоджає будь-якому іншому потоку отримувати монітор (або блокувати) для одного і того ж об'єкта , тим самим запобігаючи одночасному виконанню всіх блоків коду, захищених синхронізацією на одному об'єкті . Синхронізація також створює бар'єр пам’яті «раніше», викликаючи обмеження видимості пам’яті таким чином, що все, що робиться до того моменту, коли деяка нитка звільняє замок, з’являється в іншому потоці, згодом набуваючи той самий замок що відбувся до того, як він придбав замок. На практиці, на поточному апаратному забезпеченні це, як правило, спричиняє змивання кешів процесора при придбанні монітора і записуванні в основну пам'ять при його звільненні, і те і інше (відносно) дорого.
Використання volatile
, з іншого боку, змушує всі звернення (читання або запис) до змінної змінної відбуватися до основної пам'яті, ефективно утримуючи змінну змінну з кеш-процесорів. Це може бути корисно для деяких дій, коли просто потрібно, щоб видимість змінної була правильною, а порядок доступу не важливий. Використання volatile
також змінює лікування long
та ). З метою наочності кожен доступ до летючого поля діє як половина синхронізації.double
вимагає доступу до них, щоб бути атомним; для деяких (старих) апаратних засобів це може потребувати блокування, хоча не для сучасного 64-бітного обладнання. Відповідно до нової моделі пам'яті (JSR-133) для Java 5+, семантика енергонезалежності була посилена настільки ж сильно, як і синхронізована щодо видимості пам'яті та впорядкування інструкцій (див. Http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile
Згідно з новою моделлю пам'яті, все ще вірно, що мінливі змінні не можуть бути впорядковані між собою. Різниця полягає в тому, що тепер вже не так легко змінити звичайний доступ до поля навколо них. Запис у мінливе поле має той самий ефект пам'яті, що і випуск монітора, а читання з мінливого поля має такий же ефект пам'яті, що і монітор. По суті, через те, що нова модель пам’яті ставить більш жорсткі обмеження щодо переупорядкування доступу до летючого поля з іншими полями доступу, мінливими чи ні, все, що було видно для потоку, A
коли воно записує в мінливе поле, f
стає видимим для потоку, B
коли воно читає f
.
- поширені запитання JSR 133 (модель пам'яті Java)
Отже, тепер обидві форми бар'єру пам’яті (за чинним JMM) викликають інструкцію про повторне замовлення бар'єру, що заважає компілятору або виконувати час повторного замовлення інструкцій через бар’єр. У старому JMM, летючі не перешкоджали повторному замовленню. Це може бути важливо, тому що, крім бар'єрів пам'яті, єдиним обмеженням є те, що для будь-якого конкретного потоку чистий ефект коду такий самий, як це було б, якби інструкції були виконані саме в тому порядку, в якому вони відображаються в джерело.
Одне використання летючого речовини для спільного, але незмінного об'єкта відтворюється на льоту, при цьому багато інших потоків посилаються на об'єкт у певний момент циклу їх виконання. Іншим потрібні інші потоки, щоб почати використовувати відтворений об’єкт після його публікації, однак не потрібні додаткові накладні витрати для повної синхронізації, а це суперечка супроводу та промивання кешу.
// Declaration
public class SharedLocation {
static public SomeObject someObject=new SomeObject(); // default object
}
// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
// someObject will be internally consistent for xxx(), a subsequent
// call to yyy() might be inconsistent with xxx() if the object was
// replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published
// Using code
private String getError() {
SomeObject myCopy=SharedLocation.someObject; // gets current copy
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.
Виступаючи конкретно на запитання читання-оновлення-написання. Розглянемо наступний небезпечний код:
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
Тепер при несинхронізованому методі updateCounter () два потоки можуть вводити його одночасно. Серед багатьох перестановок того, що може статися, одна з них полягає в тому, що нитка-1 робить тест на counter == 1000 і знаходить це правдою, а потім призупиняється. Тоді нитка-2 робить той самий тест, а також бачить це правдою і призупиняється. Потім потік-1 поновлюється і встановлює лічильник на 0. Потім потік-2 поновлюється і знову встановлює лічильник 0, оскільки пропустив оновлення з потоку-1. Це також може статися, навіть якщо перемикання потоків відбувається не так, як я описав, а просто тому, що дві різні кешовані копії лічильника були присутні у двох різних ядрах процесора, і кожен потік працював на окремому ядрі. З цього питання, один потік може мати лічильник за одним значенням, а інший може мати лічильник при зовсім іншому значенні саме через кешування.
Важливим у цьому прикладі є те, що лічильник змінних зчитувався з основної пам'яті в кеш, оновлювався в кеші і лише записувався до основної пам'яті в якийсь невизначений момент пізніше, коли стався бар'єр пам'яті або коли пам'ять кешу була потрібна для чогось іншого. Створення лічильника volatile
недостатньо для безпеки потоку цього коду, оскільки тест на максимум та завдання - це дискретні операції, включаючи приріст, який є набором read+increment+write
інструкцій безатомної машини, щось на кшталт:
MOV EAX,counter
INC EAX
MOV counter,EAX
Летючі змінні корисні лише тоді, коли всі операції, які виконуються над ними, є "атомними", наприклад, мій приклад, коли посилання на повністю сформований об'єкт читається або записується (і, справді, зазвичай це пишеться лише з однієї точки). Іншим прикладом може бути посилання на мінливий масив, що підтримує список копіювання при записі, за умови, що масив зчитується лише попередньо взявши локальну копію посилання на нього.