Atomic-классы в java.util.concurrent.

Прежде чем перейти непосредственно к Atomic-классам в Java, давайте разберем наш тестовый
пример. Постепенно, по ходу нашей статьи, мы будем его изменять.
Допустим, мы собираем футбольную статистику со всех проходящих матчей и считаем количество
забитых голов. Все эти данные представлены в виде класса Statistics:

public class Statistics {
   private long goals;

public void addGoal() {
   goals++;
}

public long getGoalCount() {
   return goals;
}
}

Всю статистику собирают специальные агенты, которых я представил в виде класса FootballAgent. Все агенты работают параллельно друг другу, в нескольких потоках.

Код класса FootballAgent представлен ниже.

public class FootballAgent {
   private final Statistics statistics;

public FootballAgent(Statistics statistics) {
   this.statistics = statistics;
}

public void goal() {
   statistics.addGoal();
}

public long goalCount() {
   return statistics.getGoalCount();
}
}

Задача агентов — обновлять общую статистику, поэтому они получают в качестве параметра ссылку на объект Statistics.

Что будет, если агенты, работающие в разных потоках, начнут одновременно изменять совместно используемые данные?

Thread safety.

Thread-safety или потокобезопасность обозначает корректность выполнения программы в многопоточной среде.
Будет ли код, написанный выше вести себя корректно? Ответ — нет.
Возможно, вы уже заметили, что переменные класса Statistics никак не защищены от совместного изменения несколькими потоками, а значит могут быть повреждены.

Работу потоков нужно между собой как-то синхронизировать и дать возможность каждому из них спокойно произвести изменения, не нарушая работу другого потока.

Что если использовать volatile-переменную?

public class Statistics {
   private volatile long goals;

public void addGoal() {
   goals++;
}

public long getGoalCount() {
   return goals;
}
}

Но и в это случае ничего не изменится. Одна из популярных ошибок, связанных с переменной volatile состоит в том, что volatile гарантирует синхронизацию операций записи/чтения только для атомарных операций присваивания.

Но операция инкремента(++) состоит аж из трех операций — чтение из переменной, увеличение прочитанного значения на единицу и последующая запись в ту же самую переменную. Эти действия не
образуют атомарную операцию, а потому никаких гарантий на то, что все они успеют выполниться до того как планировщик потоков переключится на выполнение другого потока, у нас уже нет. И ключевое слово volatile таких гарантий не дает!

Для безопасного обращения с нашей переменной можно было бы все операции с ней обернуть в synchronized-блок или метод:

public class Statistics {

   private long goals;

public synchronized void addGoal() {
   goals++;
}

public synchronized long getGoalCount() {
   return goals;
}
}

Теперь действительно можно спать спокойно. Каждый поток, прежде чем изменить значение переменной goals получает блокировку на весь объект Statistics, изменяет значение переменной и отпускает блокировку. Поведение действительно стало безопасным. Но получение блокировки сама по себе довольно дорогостоящая операция. Да и блокировать весь объект только ради того, чтобы изменить значение одной переменной, слишком расточительно. Есть ли другие способы?

Java.util.concurrent.

Ответ — да. В библиотеке j.u.c. появился целый новый пакет atomic, содержащий внушительный набор классов для выполнения многих простых операций атомарно. При этом такие классы как AtomicInteger, AtomicLong, AtomicBoolean и т. д. не используют блокировки. Вместо этого в их реализации используется поддержка низкоуровневых операций типа CAS на уровне процессора.

Что такое CAS?

CAS(Compare And Set/Swap) – это атомарная инструкция процессора, при выполнении которой сначала происходит сравнение(compare) некоторого ожидаемого значения с фактическим значением в ячейке памяти. Если оно совпадает, то значения в ячейке памяти заменяется новым значением(swap). Если нет, то значение остается прежним.(особенность x86 архитектуры —значение всегда перезаписывается)
Вот как бы выглядела эта процедура, эмулированная на языке Java:

public synchronized boolean compareAndSwap(long expectedValue, long newValue) {
   long oldValue = storedValue;
   if (oldValue == expectedValue) {
      storedValue = newValue;
   return true;
}
   return false;
}

То есть, допустим один из потоков решил изменить значение нашей переменной goals. При вызове он читает старое значение переменной и получает, допустим, 3. Теперь он хочет увеличить это значение на единицу и записать его вместо старого. Для этого он вызывает операцию compareAndSwap с ожидаемым старым значением 3 и с новым значением 4. Если какой-то другой поток уже его опередил и изменил старое значение переменной раньше, то CAS не выполнит
обновления. Соответственно, этому потоку придется повторить попытку.
Вот как выглядит реализация метода getAndIncrement() в AtomicLong:

public final long getAndIncrement() {
   while (true) {
   long current = get();
   long next = current + 1;
   if (compareAndSet(current, next))
      return current;
   }
}

В цикле поток будет пытаться перезаписать значение переменной новым значением, увеличенным на единицу. Если CAS завершится успешно, то значение будет перезаписано. Если нет, то какой- то поток уже его изменил и мы пытаемся снова.

При высокой конкурентности CAS может быть уже не такой эффективной, если на переключение контекста уходит меньше времени, чем на постоянные попытки внутри бесконечного цикла.
Atomic-классы для выполнения CAS методов используют Unsafe и его специальные методы, например, compareAndSwapLong():

public final boolean compareAndSet(long expect, long update) {
   return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}

Эти методы в свою очередь уже являются нативными

Высокая конкурентность.

Для более эффективной работы в условиях высокой конкурентности в Java 8 был также добавлен специальный класс LongAdder, который однако потребляет больше памяти из-за того, что по факту он не выполняет никаких вычислений сразу. Вместо этого внутри он хранит набор всех аргументов, а сложение осуществляет только при вызове метода sum().
LongAccumulator – разновидность LongAdder с возможностью передать функцию для операций над операндами.

Эти классы не являются темой нашей статьи, но вы все же должны знать о об их существовании.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *