Списки Брайана Гетца Атомная ссылка в его книге Параллелизм 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 AtomicReferencestate = 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(UnaryOperatorupdateFunction) { V prev, next; do { prev = get(); next = updateFunction.apply(prev); } while (!compareAndSet(prev, next)); return prev; }
Метод использует ту же технику, что и метод add из класса AsyncProcessor. Он повторяет попытку метода compareAndSet в цикле, строка 6. Функция обновления будет вызываться несколько раз при сбое обновления. Таким образом, эта функция должна быть либо свободной от побочных эффектов, либо идемпотентной.
А вот метод добавления сверху, реализованный с помощью нового метода update И Get:
boolean add(AsyncSubscriptionps) { 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”