Атомарні і неатомарние операції (java) - stack overflow російською

Як зрозуміти, які операції є атомарними, а які неатомарнимі?

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

Тобто як я зрозуміла, атомарні операції - це досить дрібні, що виконуються "за один крок щодо інших потоків". Але що значить цей "крок"?

Один крок == однієї машинної операції? Або чогось іншого? Як визначити точно, які операції відносяться до атомарним, а які до неатомарним?

P.S. Я знайшла схоже запитання. але там мова йде про C #.

Як можна визначити атомарность?

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

При використанні методу nonAtomic існує ймовірність того, що якийсь потік звернеться до array [0] в той момент, коли array [0] НЕ ініціалізованим першим, і отримає несподіваного значення. При використанні probablyAtomic (за тієї умови, що масив спочатку заповнюється, а вже потім присвоюється - я зараз не можу гарантувати, що в java це саме так, але уявімо, що це правило діє в рамках прикладу) такого бути не повинно: array завжди містить або null, або проініціалізувати масив, але в array [0] не може міститися щось, крім 1. Ця операція є неподільною, і вона не може застосуватися наполовину, як це було з nonAtomic - тільки або повністю, або ніяк, і весь інший код може спокійно очікувати, що в array буде або null, або значени я, не вдаючись до додаткових перевірок.

Крім того, під атомарний операції часто мають на увазі видимість її результату всім учасникам системи, до якої це відноситься (в даному випадку - потокам); це логічно, але, на мій погляд, не є обов'язковою ознакою атомарности.

Чому це важливо?

Атомарність часто виникає з бізнес-вимог додатків: банківські транзакції повинні застосовуватися цілком, квитки на концерти замовлятися відразу в тій кількості, в якому були вказані, і т.д. Саме в тому контексті, який розбирається (многопоточность в java), завдання більш примітивні, але виростають з тих же вимог: наприклад, якщо пишеться веб-додаток, то розбирає HTTP-запити сервер повинен мати чергу вхідних запитів з атомарним додаванням, інакше є ризик втрати вхідних запитів, а, отже, і деградація якості сервісу. Атомарні операції надають гарантії (неподільності), і до них потрібно вдаватися, коли ці гарантії необхідні.

Крім того, атомарні операції лінеарізуеми - грубо кажучи, їх виконання можна розкласти в одну лінійну історію, в той час як просто операції можуть виробляти граф історій, що в ряді випадків неприпустимо.

Чому примітивні операції не є атомарними самі по собі? Так само було б простіше для всіх.

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

Тут не використовується т.зв. single source of truth для того, щоб управляти значенням Х, тому можливі такі аномалії. Наскільки розумію, читання і запис безпосередньо в пам'ять (або в пам'ять і в загальний кеш процесорів) - це якраз те, що форсує модифікатор volatile (тут можу бути неправий).

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

Це стосується лише операцій пов'язаних з установкою змінних і іншої процесорної сфері діяльності?

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

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

Будь-яка операція може бути атомарної?

Ні. Мені дуже сильно не вистачає кваліфікації для коректних формулювань, але, наскільки розумію, будь-яка операція, що має на увазі два і більше зовнішніх ефекту (сайд-ефекту), не може бути атомарної за визначенням. Під сайд-ефектом в першу чергу мається на увазі взаємодію з якоюсь зовнішньою системою (будь то файлова система або зовнішнє API), але навіть два вирази установки змінних всередині synchronized-блоку можна визнати атомарної операцією, поки одне з них може викинути виняток - а це , з урахуванням OutOfMemoryError і інших можливих результатів, може бути взагалі неможливо.

У мене операція з двома і більше сайд-ефектами. Чи можу я все-таки що-небудь з цим зробити?

Так, можна створити систему з гарантією застосування всіх операцій, але з умовою, що будь-який сайд-ефект може бути викликаний необмежену кількість разів. Ви можете створити журнальованою систему, яка атомарному записує заплановані операції, регулярно звіряється з журналом і виконує те, що ще не застосовано. Це можна представити таким чином:

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

Як все-таки визначити атомарность операцій в java?

Як зрозуміти, які операції є атомарними, а які неатомарнимі?

Ризикуючи на себе накликати звинувачення в сексизмі, таки не втримаюся і наведу приклад атомарної операції: вагітність - операція строго атомарна, завжди є один і тільки один батько (всякі генні хитрощі винесемо за дужки).

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

відповідь дан 24 Січня о 11:58

Кумедне порівняння)))) - Ksenia 24 Січня о 15:10

Ви, ймовірно, мали на увазі запліднення. з якого вагітність починається. Дуже раптовий приклад, так. - D-side 25 січ в 2:21

Загалом то так, я навмисно висловився максимально нейтрально :) - Barmaley 25 Січня в 5:58

Спробую пояснити. Можу помилятися.

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

Є глобальна змінна:

Инкрементирование змінної не є атомарної операцією: воно вимагає, як мінімум, три інструкції:

  • прочитати дані
  • збільшити на одиницю
  • записати дані

Відповідно, два потоки повинні виконати цю послідовність, але порядок їх виконання між ними не визначений. Через це і можуть виникнути ситуації на кшталт наступної:

  1. Перший потік прочитав дані
  2. Другий потік прочитав дані
  3. Перший потік збільшив значення на 1
  4. Другий потік збільшив значення на 1
  5. Другий потік записав значення
  6. Перший потік записав значення

В результаті маємо результат 1, а не 2 як очікувалося.

Щоб такого не відбувалося, використовують або синхронізацію, або атомарні примітиви з пакета java.util.concurrent

@etki правильно, проблема в тому, що не всі інструкції виконуються за один такт. І якщо комп'ютеру треба, щоб якась операція виконувалася за більше число тактів, то він може зупинитися на середині даної опереции і перейти до іншої. Наприклад, в регістр потрапило значення з якоїсь змінної i, після цього шедулера переходить до іншої задачі, а значення регістра записує в менеджер контексту. Коли він повернеться до задачі, то поверне регістру його минуле значення, неавісімо від того чим одно i. - faoxis 18 Січня о 10:43

Схожі статті