Как работают atomic-и в Java
В этой статье мы разберём, как работают AtomicInteger , AtomicLong и остальные подобные классы из пакета java . util . concurrent . atomic в Java.
Я уже частично описал их работу, здесь же будет более подробное описание, на сколько у меня получится.
На самом деле, в этих классов гораздо больше методов, чем просто get , set и compareAndSet .
Сами эти классы наследуются от java . lang . Number , а значит, они наследуют и реализуют все его методы byteValue , shortValue , intValue , longValue , floatValue , doubleValue . Однако нам интересны не они, а методы, специфичные для атомарных классов вроде AtomicInteger :
Atomic classes
Пусть есть класс, который увеличивает значение счетчика от 0 до 500:
public class AtomicTest implements Runnable < Integer counter = 0; public void run() < for (int i = 0; i < 500; i++) < counter++; >> >
Мы хотим запустить его в двух потоках:
public class AtomicTest implements Runnable < Integer counter = 0; public void run() < for (int i = 0; i < 500; i++) < counter++; >> public static void main(String[] args) throws InterruptedException < AtomicTest atomicTest = new AtomicTest(); Thread thread1 = new Thread(atomicTest); Thread thread2 = new Thread(atomicTest); thread1.start(); thread2.start(); //ждем секунду, чтобы дождаться завершения потоков, а потом напечатать результат Thread.sleep(1000); System.out.println(atomicTest.counter); >>
Казалось бы, один поток увеличивает счетчик 500 раз, второй еще 500 раз, а значит результат должен быть 1000. Но нет.
Результат выполнения вышеприведенной программы непредсказуем. Он всегда меньше 1000, у меня выводятся такие значения при нескольких запусках:
685 611 849
Объясняется это тем, что count++ не является атомарной операцией. Она состоит из чтения, увеличения на 1 и записи. В псевдокоде это можно представить так:
int temp=counter; //1. чтение counter = temp + 1; // 2. добавление единицы 3. запись
И поскольку count++ не синхронизирован, ничто не запрещает войти в этот участок двум потока одновременно. Они могут считать одно и то же значение, а потом добавить к нему 1. Например, оба потока могут сначала считать значение 0, а потом увеличить его на 1, так что итоговым результатом будет 1, а не 2.
Помочь может либо синхронизация, либо специальный класс, позволяющий сделать операцию атомарной. В данном случае это класс AtomicInteger.
Вариант c AtomicInteger
Чтобы сделать операцию атомарной, обернем счетчик counter в AtomicInteger:
public class AtomicTest implements Runnable < AtomicInteger counter = new AtomicInteger(0); public void run() < for (int i = 0; i < 500; i++) < counter.getAndIncrement(); System.out.println(counter); >> public static void main(String[] args) throws InterruptedException < AtomicTest atomicTest = new AtomicTest(); Thread thread1 = new Thread(atomicTest); Thread thread2 = new Thread(atomicTest); thread1.start(); thread2.start(); Thread.sleep(1000); System.out.println(atomicTest.counter); >>
Обратите внимание, что увеличение счетчика делается с помощью метода:
counter.getAndIncrement();
Он имеет ту же функциональность, что и синхронизация:
synchronized (this)
То есть все потоки по очереди увеличивают значение, два потока одновременно в этот участок не зайдут.
Теперь результат верный, 1000.
Но если добавить в цикл System.out.println(counter) можно заметить, что числа выводятся не по порядку (хоть итоговый результат и верный):
. 932 899 933 935 936 934 938 . 1000
Это происходит потому, что System.out.println(counter) уже не входит в синхронизированный участок кода. Синхронизируется только counter++.
Чтобы сделать вывод чисел строго последовательным, придется применить synchronized.
Вариант с synchronized
Здесь как увеличение счетчика, так и вывод в консоль чисел находится внутри блока synchronized:
public class AtomicTest implements Runnable < Integer counter = 0; public void run() < for (int i = 0; i < 500; i++) < synchronized (this) < counter++; System.out.println(counter); >> > public static void main(String[] args) throws InterruptedException < AtomicTest atomicTest = new AtomicTest(); Thread thread1 = new Thread(atomicTest); Thread thread2 = new Thread(atomicTest); thread1.start(); thread2.start(); Thread.sleep(1000); System.out.println(atomicTest.counter); >>
В результате числа выводятся строго по порядку, результат тоже 1000.
1 2 . 1000
Итоги
Мы рассмотрели AtomicInteger. По функционалу его метод:
counter.getAndIncrement();
равносилен синхронизированному коду:
synchronized (object)
Код примера есть на GitHub.
Автор sysout Опубликовано 04.10.2021 04.10.2021 Рубрики Core Java
Добавить комментарий Отменить ответ
Прошу прощения: на комментарии временно не отвечаю.
Класс AtomicInteger в Java
Этот пост будет рассматривать AtomicInteger класс в Java, который можно использовать в качестве атомарного целочисленного счетчика в многопоточных средах, доступ к которым может осуществляться одновременно несколькими потоками.
Мы знаем, что увеличение счетчика не является потокобезопасной операцией в Java. Чтобы выполнять атомарные операции над целочисленным счетчиком, мы должны синхронизировать каждую точку доступа с этим счетчиком, что значительно повлияет на производительность.
AtomicInteger Класс обеспечивает потокобезопасные операции над целочисленным счетчиком, к которому одновременно обращаются разные потоки, без использования синхронизации или сильного влияния на производительность. AtomicInteger class имеет множество служебных методов, которые являются потокобезопасными, как показано ниже:
Java Blog
Пакет java.util.concurrent.atomic — это набор классов, поддерживающих неблокирующее поточно-ориентированное программирование для отдельных переменных. По сути, классы в этом пакете расширяют понятие volatile значений, полей и элементов массива до тех, которые также обеспечивают атомарную операцию условного обновления формы:
boolean compareAndSet(expectedValue, updateValue);
Этот метод (который различается по типам аргументов в разных классах) атомарно устанавливает переменную updateValue, если она в настоящее время содержит ожидаемое значение, возвращая true в случае успеха. Классы в этом пакете также содержат методы для получения и безоговорочной установки значений, а также более слабую операцию условного атомарного обновления weakCompareAndSet.
Спецификации этих методов позволяют реализациям использовать эффективные атомарные инструкции машинного уровня, доступные на современных процессорах. Однако на некоторых платформах поддержка может повлечь за собой некоторую форму внутренней блокировки. Таким образом, не гарантируется, что методы будут неблокирующими — поток может временно заблокироваться перед выполнением операции.
Каждый из экземпляров классов AtomicBoolean, AtomicInteger, AtomicLong и AtomicReference обеспечивает доступ и обновления к одной переменной соответствующего типа. Каждый класс также предоставляет соответствующие служебные методы для этого типа. Например, классы AtomicLong и AtomicInteger предоставляют методы атомарного приращения. Одно из применений — создание порядковых номеров, например:
class Sequencer < private final AtomicLong sequenceNumber = new AtomicLong(0); public long next() < return sequenceNumber.getAndIncrement(); >>
Несложно определить новые служебные функции, которые, как и getAndIncrement, атомарно применяют функцию к значению. Например, учитывая некоторую трансформацию
long transform(long input)
можно написать следующее:
long getAndTransform(AtomicLong var) < long prev, next; do < prev = var.get(); next = transform(prev); >while (!var.compareAndSet(prev, next)); return prev; // return next; для transformAndGet >
Эффекты памяти для доступа и обновления атомарных переменных обычно соответствуют правилам для volatile, как указано в Спецификации языка Java (модель памяти 17.4):
- get имеет эффекты памяти при чтении volatile переменной.
- set имеет эффекты памяти записи (назначения) volatile переменной.
- lazySet имеет эффекты памяти записи (назначения) volatile переменной, за исключением того, что он позволяет переупорядочивать с последующими (но не предыдущими) действиями с памятью, которые сами по себе не накладывают ограничений переупорядочения с обычными не-volatile записями. Среди других контекстов использования lazySet может применяться при обнулении для сбора мусора ссылки, к которой больше никогда не будет доступа.
- weakCompareAndSet атомарно считывает и записывает переменную по условию, но не создает каких-либо упорядоченных операций (happens-before orderings), поэтому не дает никаких гарантий относительно предыдущих или последующих операций чтения и записи любых переменных, кроме цели weakCompareAndSet.
- compareAndSet и все другие операции чтения и обновления, такие как getAndIncrement, имеют эффекты памяти как чтения, так и записи volatile переменных.
В дополнение к классам, представляющим отдельные значения, этот пакет содержит классы Updater, которые можно использовать для получения операций compareAndSet в любом выбранном volatile поле любого выбранного класса. AtomicReferenceFieldUpdater, AtomicIntegerFieldUpdater и AtomicLongFieldUpdater — это утилиты на основе отражения, которые обеспечивают доступ к связанным типам полей. В основном они используются в атомарных структурах данных, в которых несколько volatile полей одного и того же узла (например, ссылки узла дерева) независимо подвергаются атомарным обновлениям. Эти классы обеспечивают большую гибкость в том, как и когда использовать атомарные обновления, за счет более неуклюжей настройки на основе отражения, менее удобного использования и более слабых гарантий.
Классы AtomicIntegerArray, AtomicLongArray и AtomicReferenceArray дополнительно расширяют поддержку атомарных операций для массивов этих типов. Эти классы также примечательны тем, что предоставляют семантику volatile доступа для своих элементов массива, которая не поддерживается для обычных массивов.
Атомарные классы также поддерживают метод weakCompareAndSet, который имеет ограниченную применимость. На некоторых платформах weak версия может быть более эффективной, чем compareAndSet в обычном случае, но отличается тем, что любой конкретный вызов метода weakCompareAndSet может ложно возвращать false значение (то есть без видимой причины). Возврат false означает только то, что операцию можно повторить при желании, полагаясь на гарантию того, что повторный вызов, когда переменная содержит ожидаемое значение и ни один другой поток также не пытается установить переменную, в конечном итоге будет успешным. (Такие ложные сбои могут быть, например, из-за эффектов конкуренции за память, которые не связаны с тем, равны ли ожидаемые и текущие значения.) Кроме того, weakCompareAndSet не обеспечивает гарантии упорядочения, которые обычно необходимы для управления синхронизацией. Однако этот метод может быть полезен для обновления счетчиков и статистики, когда такие обновления не связаны с другими событиями, происходящими до упорядочения программы. Когда поток видит обновление атомарной переменной, вызванное weakCompareAndSet, он не обязательно видит обновления любых других переменных, которые произошли до weakCompareAndSet. Это может быть приемлемо, например, при обновлении статистики производительности, но редко в противном случае.
Класс AtomicMarkableReference связывает одно логическое значение со ссылкой. Например, этот бит может использоваться внутри структуры данных для обозначения того, что объект, на который имеется ссылка, был логически удален. Класс AtomicStampedReference связывает целочисленное значение со ссылкой. Это может быть использовано, например, для представления номеров версий, соответствующих серии обновлений.
Атомарные классы разработаны в первую очередь как строительные блоки для реализации неблокирующих структур данных и связанных классов инфраструктуры. Метод compareAndSet не является общей заменой блокировки. Он применяется только тогда, когда критические обновления объекта ограничиваются одной переменной.
Атомарные классы не являются заменой общего назначения для java.lang.Integer и связанных классов. Они не определяют такие методы, как equals, hashCode и compareTo. (Поскольку ожидается, что атомарные переменные будут видоизменяться, они не подходят для ключей хэш-таблицы.) Кроме того, классы предоставляются только для тех типов, которые обычно используются в предполагаемых приложениях. Например, не существует атомарного класса для представления байта. В тех редких случаях, когда вы хотели бы это сделать, вы можете использовать AtomicInteger для хранения байтовых значений и соответствующего преобразования. Вы также можете хранить float, используя преобразования Float.floatToRawIntBits(float) и Float.intBitsToFloat(int), и double с помощью преобразований Double.doubleToRawLongBits(double) и Double.longBitsToDouble(long).
- Spring Resource: контексты приложения и пути ресурсов, создание контекстов приложения
- Spring Resource: предостережения относительно FileSystemResource
- Модульное (юнит) тестирование в Spring