Рубрики
Без рубрики

Атомарная ссылка, иногда более простая альтернатива синхронизированным блокам

Брайан Гетц перечисляет AtomicReference в своей книге Java Concurrency на практике в разделе in… Помеченный java.

Списки Брайана Гетца Атомная ссылка в его книге Параллелизм Java на практике в разделе дополнительные темы. Тем не менее, мы увидим, что AtomicReference для конкретных случаев использования проще в использовании, чем синхронизированные блоки. А новые методы JDK8 get И Update и updateAndGet еще больше упрощают использование AtomicReference.

Но давайте начнем с первой темы, варианта использования, который может быть проще реализовать с помощью AtomicReference, чем с помощью синхронизированного блока: параллельный конечный автомат.

Класс Средство чтения команд из плагина maven surefire использует метод compareAndSet для реализации параллельного конечного автомата:

public final class CommandReader {
  private static final CommandReader READER = new CommandReader();
  private final Thread commandThread = 
    newDaemonThread( new CommandRunnable(), "surefire-forkedjvm-command-thread" );
  private final AtomicReference state =
   new AtomicReference( NEW );
  public static CommandReader getReader() {
     final CommandReader reader = READER;
     if ( reader.state.compareAndSet( NEW, RUNNABLE ) ) {
         reader.commandThread.start();
     }
   return reader;
  }
}

Класс AtomicReference обертывает другой класс, чтобы обогатить переменную функциональностью атомарного обновления. В строке 5 AtomicReference представляет атомарную переменную потока типа Enum. Государство. AtomicReference инициализируется в строке 6 значением NEW.

Метод getReader должен запускать командный поток, когда текущее состояние является НОВЫМ, и обновлять его значение до RUNNABLE. Поскольку метод может вызываться несколькими потоками параллельно, настройка и проверка должны выполняться атомарно. Это делается с помощью метода compareAndSet, строка 9. Метод compareAndSet обновляет свое значение до нового значения только тогда, когда текущее значение совпадает с ожидаемым. В приведенном примере он обновляет переменную до RUNNING только тогда, когда текущее значение является НОВЫМ. Если обновление выполнено успешно, метод возвращает true, и поток запускается, в противном случае он возвращает false, и ничего не происходит. Проверка и обновление выполняются атомарно.

Вот, в качестве сравнения, та же функциональность, реализованная с помощью синхронизированного блока.

public final class CommandReader {
 private static final CommandReader READER = new CommandReader();
 private final Thread commandThread = 
   newDaemonThread( new CommandRunnable(), "surefire-forkedjvm-command-thread" );
 private final  Thread.State state =  NEW;
 private final Object LOCK = new Object();
 public static CommandReader getReader()  {
    final CommandReader reader = READER;
    synchronized(reader.LOCK) {
      if(reader.state == NEW) {
         reader.commandThread.start();
         reader.state = RUNNABLE;
      }
    }
    return reader;
 }
}

Мы используем синхронизированный блок вокруг проверки и обновления состояния переменной, строка 10. Этот пример показывает, почему нам нужно выполнять атомарную проверку и обновление. Без синхронизации два потока могут считывать новое состояние, вызывая метод start из командного потока несколько раз.

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

Идея использования compareAndSet для обновлений заключается в повторении попыток до тех пор, пока обновление не завершится успешно. Класс AsyncProcessor из RxJava использует этот метод для обновления массива подписчиков в методе add:

final AtomicReference[]> subscribers;
boolean add(AsyncSubscription ps) {
 for (;;) {
   AsyncSubscription[] a = subscribers.get();
   if (a == TERMINATED) {
     return false;
   }
   int n = a.length;
   @SuppressWarnings("unchecked")
   AsyncSubscription[] b = new AsyncSubscription[n + 1];
   System.arraycopy(a, 0, b, 0, n);
   b[n] = ps;
   if (subscribers.compareAndSet(a, b)) {
     return true;
    }
  }
}

Обновление повторяется с использованием цикла for, строка 3. Цикл завершается только в том случае, если либо массив абонентов находится в состоянии завершено, строка 6, либо операция compareAndSet завершается успешно, строка 14. Во всех остальных случаях обновление повторяется для копии массива.

Начиная с JDK 8 класс AtomicReference предоставляет эту функциональность в двух служебных методах get И Update и updateAndGet Ниже показана реализация метода get И Update в JDK 8:

public final V getAndUpdate(UnaryOperator updateFunction) {
 V prev, next;
 do {
   prev = get();
   next = updateFunction.apply(prev);
 } while (!compareAndSet(prev, next));
 return prev;
}

Метод использует ту же технику, что и метод add из класса AsyncProcessor. Он повторяет попытку метода compareAndSet в цикле, строка 6. Функция обновления будет вызываться несколько раз при сбое обновления. Таким образом, эта функция должна быть либо свободной от побочных эффектов, либо идемпотентной.

А вот метод добавления сверху, реализованный с помощью нового метода update И Get:

boolean add(AsyncSubscription ps) {
AsyncSubscription[] result = subscribers.updateAndGet(  ( a ) ->  {  
  if (a != TERMINATED) {       
    int n = a.length;
    @SuppressWarnings("unchecked")
    AsyncSubscription[] b = new AsyncSubscription[n + 1];
    System.arraycopy(a, 0, b, 0, n);
    b[n] = ps;
    return b;
  }
  else {
    return a;
  }
});
return result != TERMINATED;    
}

Как мы видим, цикл while скрыт в методе update И Get. Нам нужно только реализовать функцию, вычисляющую новое значение из старого.

Мы видели два примера compareAndSet. Если вас интересуют дополнительные примеры, взгляните на книгу Искусство многопроцессорного программирования . В нем показано, как реализовать типичные параллельные структуры данных с помощью compareAndSet. И в этой статье показано, как тестировать атомарные обновления.

Я был бы рад услышать от вас о том, как вы используете AtomicReference в своем приложении.

Оригинал: “https://dev.to/vmlensd/atomicreference-a-sometimes-easier-alternative-to-synchronized-blocks-538c”