Войти
Написать нам в Viber

Многопоточность в Java 8. Часть 1 (Атомарные типы)

В этой статье будет рассказано про важные улучшения в Concurrency API связанное с атомарными типами.
         Для упрощение примеров кода будем использовать два вспомогательных метода sleep(seconds) и stop(executor) для остановки выполнения и освобождения ресурсов executor.
 
Atomic классы
 
         В пакете concurrent находиться большое количество классов для выполнения атомарных операций. Операция считается атомарной, когда вы можете безопасно выполнять ее в многопоточной среде без использования разных способов блокировки.
 
         Внутри атомарных типов (классов) интенсивно используется операция сравнение с обменом (CAS – compare-and-swap),  она реализована на самом низком уровне как инструкция процессора. Эта атомарная инструкции, как правило, гораздо быстрее, чем синхронизация через блокировку. Так что атомарные типы лучше использовать вместо блокировки в том случае, когда нужно просто модифицировать одну переменную одновременно.
 
         Теперь давайте разберем один из этих классов на нескольких примерах: AtomicLong.
         Также примем неявное объявление нескольких повторяющихся переменных в последующих примерах:
 

int processors = Runtime.getRuntime().availableProcessors();  AtomicLong num = new AtomicLong (0);
         ExecutorService exec = Executors.newFixedThreadPool(processors);
 

 
 

 
  for (int i = 0; i < 100; i++) {
            exec.submit(num::incrementAndGet);
  }
 
stop(exec);
 
System.out.println(num.get()); // => 100

 
 
         При использовании AtomicLong в качестве замены для Long мы можем увеличивать число параллельно и потокобезопасно без синхронизации доступа к переменной. Метод incrementAndGet () является атомарной операцией, поэтому мы можем смело вызвать этот метод из нескольких потоков.
 
         AtomicLong предоставляет большой набор методов для осуществления атомарных операций. К примеру, метод updateAndGet () принимает лямбда-функцию для того, чтобы выполнить произвольные арифметические операции над целыми числами:
 

 
  for (int i = 0; i < 100; i++) {
            exec.submit(() -> num.updateAndGet(v -> v + 3));
    }
stop(exec);
System.out.println(num.get()); // 300

 
            Метод accumulateAndGet() принимает другой тип функции класса IntBinaryOperator. Мы используем этот метод, чтобы суммировать все числа от 0 до 100 одновременно в следующем примере:

 
 for (int i = 0; i < 100; i++) {
       exec.submit(() -> num.accumulateAndGet(i, (sum, curr) -> sum + curr));
 }
 
stop(exec);
System.out.println(num.get()); // 4950

 
Также есть и другие полезные атомарные классы AtomicBoolean, AtomicInteger и AtomicReference.
 
 
 
Adder классы
 
         Теперь давайте перейдем к новым типам, которые были добавлены в JDK 8, а именно классам сумматорам (Adder) и аккумуляторам (Accumulator), и как пример рассмотрим LongAdder.
          Класс LongAdder, в качестве альтернативы AtomicLong, можно использовать для последовательного добавления значений.

 
LongAdder adder = new LongAdder();
 for (int i = 0; i < 100; i++) {
      exec.submit(adder::increment));
 }
 
 stop(exec);
 System.out.println(adder.sumThenReset()); // 100

 
         LongAdder предоставляет методы add() и increment() такие же как у атомарных типов, рассмотренных в предыдущем разделе, которые тоже являются потокобезопасными. Но вместо того чтобы просуммировать все значения в конечный результат этот класс хранит в себе набор переменных, чтобы уменьшить конкуренцию за ресурс между потоками. Фактический результат может быть получен путем вызова методов sum() или sumThenReset().
 
         Этот класс, как правило, предпочтительнее обычных атомарных типов, когда обновления из нескольких потоков являются более частым, чем чтение. Это часто происходит при сборе статистических данных, например, вы хотите подсчитать количество запросов, обслуживаемых на веб-сервере. Недостатком LongAdder это высокое потребление памяти по сравнению с AtomicLong, поскольку набор переменных находиться в памяти.
 
Accumulator классы
 
         LongAccumulator является более обобщенной версией LongAdder. Вместо выполнения простой операции добавления класса LongAccumulator построен на арифметическом лямбда-выражение типа LongBinaryOperator, как показано в этом примере кода:
 

 
 LongBinaryOperator func = (acc, c) -> acc + c * c;
 LongAccumulator accum = new LongAccumulator(func, 1);
 
for (int i = 0; i < 100; i++) {
      exec.submit(() -> accum.accumulate(i)));
}
 
 stop(exec);
 System.out.println(accum.getThenReset()); // 328351

 
         Мы создаем LongAccumulator с функцией acc + c*c и с начальным значением равным единице. При каждом вызове accumulate(i), текущий результат и значение i передаются как параметры в указанную выше функцию.
 
         LongAccumulator так же, как и LongAdder сохраняют набор переменных внутри, чтобы уменьшить взаимодействие между потоками.
         Помимо Long классов есть еще Double сумматоры и аккумуляторы – DoubleAdder и DoubleAccumulator.