Как понять, какие операции являются атомарными, а какие неатомарными?
Вот что я нашла на Хабре:
Операция в общей области памяти называется атомарной, если она
завершается в один шаг относительно других потоков, имеющих доступ к
этой памяти. Во время выполнения такой операции над переменной, ни
один поток не может наблюдать изменение наполовину завершенным.
Атомарная загрузка гарантирует, что переменная будет загружена целиком
в один момент времени. Неатомарные операции не дают такой гарантии.
Т.е. как я поняла, атомарные операции - это достаточно мелкие, выполняющиеся "з
один шаг относительно других потоков". Но что значит этот "шаг"?
Один шаг == одной машинной операции? Или чему-то другому? Как определить точно, какие операции относятся к атомарным, а какие к неатомарным?
P.S.: Я нашла похожий вопрос, но там речь идёт о C#...
Ответы
Ответ 1
Как можно определить атомарность?
Атомарность операции чаще всего принято обозначать через ее признак неделимости
операция может либо примениться полностью, либо не примениться вообще. Хорошим примером будет запись значений в массив:
public class Curiousity {
public volatile int[] array;
public void nonAtomic() {
array = new int[1];
array[0] = 1;
}
public void probablyAtomic() {
array = new int[] { 1 };
}
}
При использовании метода nonAtomic существует вероятность того, что какой-то пото
обратится к array[0] в тот момент, когда array[0] не проинициализирован, и получит неожиданно
значение. При использовании probablyAtomic (при том условии, что массив сначала заполняется
а уже потом присваивается - я сейчас не могу гарантировать, что в java это именно так
но представим, что это правило действует в рамках примера) такого быть не должно: array всегда содержит либо null, либо проинициализированный массив, но в array[0] не может содержаться что-то, кроме 1. Эта операция неделима, и она не может примениться наполовину, как это было с nonAtomic - только либо полностью, либо никак, и весь остальной код может спокойно ожидать, что в array будет либо null, либо значения, не прибегая к дополнительным проверкам.
Кроме того, под атомарностью операции зачастую подразумевают видимость ее результат
всем участникам системы, к которой это относится (в данном случае - потокам); это логично, но, на мой взгляд, не является обязательным признаком атомарности.
Почему это важно?
Атомарность зачастую проистекает из бизнес-требований приложений: банковские транзакци
должны применяться целиком, билеты на концерты заказываться сразу в том количестве
в котором были указаны, и т.д. Конкретно в том контексте, который разбирается (многопоточност
в java), задачи более примитивны, но произрастают из тех же требований: например, если пишется веб-приложение, то разбирающий HTTP-запросы сервер должен иметь очередь входящих запросов с атомарным добавлением, иначе есть риск потери входящих запросов, а, следовательно, и деградация качества сервиса. Атомарные операции предоставляют гарантии (неделимости), и к ним нужно прибегать, когда эти гарантии необходимы.
Кроме того, атомарные операции линеаризуемы - грубо говоря, их выполнение можно разложит
в одну линейную историю, в то время как просто операции могут производить граф историй, что в ряде случаев неприемлимо.
Почему примитивные операции не являются атомарными сами по себе? Так же было бы проще для всех.
Современные среды исполнения очень сложны и имеют на борту некислый ворох оптимизаций
которые можно сделать с кодом, но, в большинстве случаев, эти оптимизации нарушают гарантии
Так как большинство кода этих гарантий на самом деле не требует, оказалось проще выделит
операции с конкретными гарантиями в отдельный класс, нежели наоборот. Чаще всего в приме
приводят изменение порядка выражений - процессор и JVM имеют право выполнять выражения не в том порядке, в котором они были описаны в коде, до тех пор, пока программист не будет форсировать определенный порядок выполнения с помощью операций с конкретными гарантиями. Также можно привести пример (не уверен, правда, что формально корректный) с чтением значения из памяти:
thread #1: set x = 2
processor #1: save_cache(x, 2)
processor #1: save_memory(x, 2)
thread #2: set x = 1
processor #2: save_cache(x, 1)
processor #2: save_memory(x, 1)
thread #1: read x
processor #1: read_cache(x) = 2 // в то время как х уже был обновлен значением
в thread #2
Здесь не используется т.н. single source of truth для того, чтобы управлять значение
Х, поэтому возможны такие аномалии. Насколько понимаю, чтение и запись напрямую в память (или в память и в общий кэш процессоров) - это как раз то, что форсирует модификатор volatile (здесь могу быть неправ).
Конечно, оптимизированный код выполняется быстрее, но необходимые гарантии никогда не должны приноситься в жертву производительности кода.
Это относится только к операциям связанным с установкой переменных и прочей процессорной сфере деятельности?
Нет. Любая операция может быть атомарной или неатомарной, например, классически
реляционные базы данных гарантируют, что транзакция - которая может состоять из изменени
данных на мегабайты - либо применится полностью, либо не будет применена. Процессорны
инструкции здесь не имеют никакого отношения; операция может быть атомарной до тех пор, пока она является атомарной сама по себе или ее результат проявляется в виде другой атомарной операции (например, результат транзакции базы данных проявляется в записи в файл).
Кроме того, насколько понимаю, утверждение "инструкция не успела за один цикл - операци
неатомарна" тоже неверно, потому что есть некоторые специализированные инструкции, и никто не мешает атомарно устанавливать какое-либо значение в памяти на входе в защищенный блок и снимать его по выходу.
Любая ли операция может быть атомарной?
Нет. Мне очень сильно не хватает квалификации для корректных формулировок, но, наскольк
понимаю, любая операция, подразумевающая два и более внешних эффекта (сайд-эффекта)
не может быть атомарной по определению. Под сайд-эффектом в первую очередь подразумеваетс
взаимодействие с какой-то внешней системой (будь то файловая система или внешнее API), но даже два выражения установки переменных внутри synchronized-блока нельзя признать атомарной операцией, пока одно из них может выкинуть исключение - а это, с учетом OutOfMemoryError и прочих возможных исходов, может быть вообще невозможно.
У меня операция с двумя и более сайд-эффектами. Могу ли я все-таки что-нибудь с этим сделать?
Да, можно создать систему с гарантией применения всех операций, но с условием, чт
любой сайд-эффект может быть вызван неограниченное число раз. Вы можете создать журналируемую систему, которая атомарно записывает запланированные операции, регулярно сверяется с журналом и выполняет то, что еще не применено. Это можно представить следующим образом:
client: journal.push {withdrawMoney {card=41111111, cvc=123}, reserveTicket {concert=123}, sendEmail {address=nobody@localhost}}
client: <журнал подтвердил получение и запись задания>
journal: process withdrawMoney
journal: markCompleted withdrawMoney
journal: process reserveTicket
journal: <умирает, не успев записать выполнение reserveTicket>
journal: <восстанавливается>
journal: process reserveTicket # сайд-эффект вызывается еще раз, но только в случае некорректной работы
journal: markCompleted reserveTicket
journal: process sendEmail
journal: markCompleted sendEmail
Это обеспечивает прогресс алгоритма, но снимает все обязательства по временным рамка
(с которыми, формально говоря, и без того не все в порядке). В случае, если операции идемпотентны, подобная система будет рано или поздно приходить к требуемому состоянию без каких-либо заметных отличий от ожидаемого (за исключением времени выполнения).
Как все-таки определить атомарность операций в java?
Первичный источник правды в этом случае - это Java Memory Model, которая определяет
какие допущения и гарантии применяются к коду в JVM. Java Memory Model, впрочем, довольн
сложна для понимания и покрывает значительно большую сферу операций, нежели сфера атомарны
операций, поэтому в контексте этого вопроса достаточно знать, что модификатор volatile обеспечивает атомарное чтение и запись, а классы Atomic* позволяют производить compare-and-swap операции, чтобы атомарно менять значения, не боясь, что между чтением и записью придет еще одна чья-то запись, а в комментариях ниже на момент прочтения наверняка добавили еще что-то, что я забыл.
Ответ 2
Как понять, какие операции являются атомарными, а какие неатомарными?
Рискуя на себя навлечь обвинения в сексизме, таки не удержусь и приведу пример атомарно
операции: беременность - операция строго атомарная, всегда есть один и только один отец (всякие генные ухищрения вынесем за скобки).
И наоборот пример неатомарной операции: воспитание ребенка - увы операция неатомарная
ребенок есть к сожалению субъект множества различных несинхронизированных операций над неокрепшей душой ребенка: мама, папа, бабушка, дедушка, зомбоящик, детсад, школа, друзья, подруги и т.д. по списку.
Ответ 3
Попробую объяснить. Могу ошибаться.
Есть java, исходники компилируются в байткод. Байткод во время выполнения программ
преобразуется в машинный код. Одна инструкция/команда в байткоде может преобразоватьс
в несколько инструкций машинного кода. В этом и заключается проблема атомарности. Процессо
не может за раз выполнить одну команду, написанную на языке высокого уровня: он выполняет машинный код, содержащий последовательность команд. Следовательно, если разные процессоры выполняют манипуляции над одними и теми же данными, то различные инструкции процессоров могут чередоваться.
Приведу пример:
Есть глобальная переменная:
public volatile int value = 0;
first-thread {
value++
}
second-thread{
value++
}
Инкрементирование переменной не является атомарной операцией: оно требует, как минимум, три инструкции:
прочитать данные
увеличить на единицу
записать данные
Соответственно, два потока должны выполнить эту последовательность, но порядок и
выполнения между ними не определен. Из-за этого и могут возникнуть ситуации вроде следующей:
Первый поток прочел данные
Второй поток прочел данные
Первый поток увеличил значение на 1
Второй поток увеличил значение на 1
Второй поток записал значение
Первый поток записал значение
В результате имеем результат 1, а не 2 как ожидалось.
Чтобы такого не происходило, используют либо синхронизацию, либо атомарные примитивы из пакета java.util.concurrent
Комментариев нет:
Отправить комментарий