вівторок, 9 серпня 2011 р.

Atomic-типы

Давным-давно я обещал написать обзорный цикл по java.util.concurrent.*, да как-то и застряло. Встречайте... Atomic-типы!

Как известно, операции над переменными нельзя считать атомарными, если только это не присвоение примитивных типов (и то - с оговорками на разрядность).
Почему так, можно понять, например, изучив байт-код операций (см. например Как выглядит счетчик в байт-коде?).
Для упрощения программы в случае, когда синхронизация потоков осуществляется на основе значения некоторой переменной, например, счетчика, были добавлены типы из пакета java.util.concurrent.atomic.*, а именно:

AtomicInteger (тут все ясно)
AtomicLong (тут тоже)
AtomicBoolean (яснее некуда)
AtomicReference (атомная ссылка на экземпляр чего-либо, не имеет отношения к java.lang.ref.Reference)

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

Несмотря на отсутствие общего родителя (Serializable не в счет), у всех есть несколько общих методов, которые выполняются атомарно, а именно:

compareAndSet - сравнивает хранимое значение с указанным и если они совпалают, -устанавливает новое значеие

weakCompareAndSet - хитрый метод, похож по семантике на compareAndSet, но со странной оговоркой "May fail spuriously". О нем ниже.

getAndSet - изменяет текущее значение, и возвращает старое

get и set - соответственно, атомарные чтение и запись значений, по семантике аналогичны чтению и записи в volatile-переменную

lazySet - тоже весьма интересный метод, обещает когда-нибудь да установить значение в памяти.
Несмотря на описание в javadoc, что метод имеет семантику volatile-записи, он больше похож как раз на запись в не-volatile переменную. Другие потоки могут не сразу увидеть изменения - пока не наступит синхронизирующее событие (например, volatile-запись или выход из блока synchronized). Ниша этого метода - обнуление ссылок на обьекты, когда другие потоки не зависят от быстрого обновления ссылки, но вы хотите чтобы GC собрал обьект. В таком случае, эффективнее использовать lazySet(null), вместо set(null). Есть намеки на другие применения, но разбираться мы с этим будем как нибудь в другой раз.

У AtomicInteger и AtomicLong есть следующие пары методов, назначение которых вполне понятно из названий:

incrementAndGet  - аналог ++i
getAndIncrement - аналог i++
decrementAndGet и getAndDecrement - соответственно --i и i++
addAndGet и getAndAdd - атомарная аккумуляция, разница, что делается в первую очередь - определение, что возвращать, или изменение значение.

В целом, в методах xxxAndGet возвращается измененное значение, в методах getAndXxx - изначальное.

AtomicReference ничем не примечателен, кроме того, что параметризирован.

Наша теперь задача - понять, как это помогает потокам сохранить свою живость в жестоком многопоточном мире.

Атомарные типы все построены вокруг хитрой операции процессоров Intel и бывшей Sun, которая называется compare-and-swap. Операция берет три аргумента - адрес памяти, ожидаемое значение и новое значение, - сравнивет содержимое памяти по адресу с ожидаемым значением и если оны равны - записывает в память новое значение. Операция атомарна. Впервые такая операция появилась в мейнфреймах IBM/370, с помощью CAS можно реализовать синхронизацию без использования блокировок по следующей схеме - процессор читает некое значение в памяти, выполняет над ним действие в собственных регистрах, и выполняет CAS в ту же ячейнку, ожидая там старое значение. Если проверка закончилась неудачно, данные читаются снова, снова выполняется операция, и опять пробуется CAS. Таким образом операция всегда происходит над свежими данными и соблюдается свойство атомарности.

Метод compareAndSet есть мостиком к этой инструкции, а изучив исходный код JDK, мы увидим те самые циклы, например (заметьте - блокировки не используются):

    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

Именно таким образом следует работать с Atomic типами, если вы хотите определить свои операции над ними, например, деление. Кстати, в отличие от обычных оберточных типов, атомики не помечены как final, что как бы намекает...


Чем отличается compareAndSet от weakCompareAndSet?

Суть в том, что на архитектурах MIPS, ARM, PowerPC и Alpha используется другой подход к реализации неблокирующей синхронизации, известный как Load-link/Store-Conditional. Операция подобная CAS на таких процессорах выполняется с помощью цикла из двух инструкций загрузки LL и записи SC, подобно тому, как это было показано выше. По различным обстоятельствам, которые могут произойти между двумя инструкциями, запись может завершится с ошибкой. Некоторые алгоритмы не требуют такого цикла и могут быть выполнены эффективнее, даже если допустить спонтанные ошибки записи.
К сожалению, в исходниках JDK weakCompareAndSet реализован точно так же как и compareAndSet, однако подразумевается, что на некоторых архитектурах процессоров этот метод может работать быстрее, хотя и допускает непредсказуемые ошибки записи. Я попробую разобраться в теме в одной из следующих статей цикла.

На этом все, надеюсь вам было так же интересно, как и мне.


Эти ссылки были мне особенно полезны:

http://cs.oswego.edu/pipermail/concurrency-interest/2006-February/002276.html

http://cs.oswego.edu/pipermail/concurrency-interest/2006-May/002501.html

http://stackoverflow.com/questions/1468007/atomicinteger-lazyset-and-set

6 коментарів:

  1. Различие между compareAndSet и weakCompareAndSet не только (и даже не столько) в том, что второй допускает "беспричинные отказы". Очень важное отличие еще в том, что weak версия _не гарантирует_ создание ребра happens-before. Обычный CAS во-первых делает атомарное обновление, а во-вторых (неявно) делает full fence. weak версия _может_ не делать full fence, если процессор поддерживает такой вариант инструкции (например, их поддерживает
    Azul box)

    Подробнее здесь: http://cheremin.blogspot.com/2011/07/weakcompareandset.html

    С lazySet все тоже сложнее -- http://cheremin.blogspot.com/2011/09/atomicxxxlazyset-strikes-back.html

    ВідповістиВидалити
  2. AtomicInteger (тут все ясно)
    AtomicLong (тут тоже)
    AtomicBoolean (яснее некуда)

    Дебил какой-то, так объясняет материал. ИМХО. Было бы ясно не гуглил бы!!!

    ВідповістиВидалити
    Відповіді
    1. Сам дебил.

      атомическое целое, длинное и булевское. Мне как раз дебилизмом показалось переводить английские слова, но видимо просчитался

      Видалити
  3. А можно рассказать, в чем отличие АтомикИнтеджер от просто Интеджер? Понятно, что в названии, а в работе они как отличаются?

    ВідповістиВидалити