Когда использовать AtomicReference в Java?

311

Когда мы используем AtomicReference?

Нужно ли создавать объекты во всех многопоточных программах?

Приведите простой пример использования AtomicReference.

Chintu
источник

Ответы:

215

Атомная ссылка должна использоваться в настройках, где вам нужно выполнять простые атомарные (т. Е. Поточно- ориентированные, нетривиальные) операции над ссылкой, для которых синхронизация на основе монитора не подходит. Предположим, вы хотите проверить, является ли определенное поле только в том случае, если состояние объекта остается таким, как вы в последний раз проверяли

AtomicReference<Object> cache = new AtomicReference<Object>();

Object cachedValue = new Object();
cache.set(cachedValue);

//... time passes ...
Object cachedValueToUpdate = cache.get();
//... do some work to transform cachedValueToUpdate into a new version
Object newValue = someFunctionOfOld(cachedValueToUpdate);
boolean success = cache.compareAndSet(cachedValue,cachedValueToUpdate);

Из-за атомарной ссылочной семантики вы можете сделать это, даже если cacheобъект является общим для потоков, без использования synchronized. В общем, вам лучше использовать синхронизаторы или java.util.concurrentфреймворк, чем голые, Atomic*если вы не знаете, что делаете.

Две отличные ссылки о мертвом дереве, которые познакомят вас с этой темой:

Обратите внимание, что (я не знаю, было ли это всегда так), присвоение ссылки (то есть =) само по себе является атомарным (обновление примитивных 64-битных типов, таких как longили doubleможет не быть атомарным, но обновление ссылки всегда атомарно, даже если оно 64-битное ) без явного использования Atomic*.
См. Спецификацию языка Java 3ed, Раздел 17.7 .

andersoj
источник
43
Поправьте меня, если я ошибаюсь, но похоже, что ключ к этому нужен, потому что вам нужно сделать «compareAndSet». Если бы все, что мне нужно было сделать, было установлено, мне бы вообще не понадобился AtomicObject, потому что сами обновления ссылок являются атомарными?
sMoZely
Безопасно ли делать cache.compareAndSet (cachedValue, someFunctionOfOld (cachedValueToUpdate))? Т.е. встроенные вычисления?
Kaqqao
4
@veggen Аргументы функции в Java оцениваются перед самой функцией, поэтому в этом случае вставка не имеет значения. Да, это безопасно.
Дмитрий
29
@sMoZely Это правильно, но если вы не используете, AtomicReferenceвы должны пометить переменную, volatileпотому что, хотя среда выполнения гарантирует, что присвоение ссылки является атомарным, компилятор может выполнять оптимизацию в предположении, что переменная не изменялась другими потоками.
Кболино
1
@BradCupit заметьте, что я сказал "если вы не используете AtomicReference"; если вы будете использовать его, то мой совет будет идти в направлении , противоположного и пометить его , finalтак что компилятор может оптимизировать соответственно.
Кболино
91

Атомная ссылка идеальна для использования, когда вам нужно совместно использовать и изменять состояние неизменяемого объекта между несколькими потоками. Это очень плотное утверждение, поэтому я его немного разобью.

Во-первых, неизменный объект - это объект, который практически не изменяется после построения. Часто методы неизменяемого объекта возвращают новые экземпляры этого же класса. Некоторые примеры включают классы-обёртки Long и Double, а также String, и это лишь некоторые из них. (Согласно данным Programm Concurrency на JVM неизменяемые объекты являются важной частью современного параллелизма).

Далее, почему AtomicReference лучше, чем изменчивый объект для совместного использования этого общего значения. Простой пример кода покажет разницу.

volatile String sharedValue;
static final Object lock=new Object();
void modifyString(){
  synchronized(lock){
    sharedValue=sharedValue+"something to add";
  }
}

Каждый раз, когда вы хотите изменить строку, на которую ссылается это изменчивое поле, основываясь на его текущем значении, вам сначала нужно получить блокировку для этого объекта. Это препятствует тому, чтобы какой-то другой поток вошел в это время и изменил значение в середине новой конкатенации строк. Затем, когда ваш поток возобновляется, вы забиваете работу другого потока. Но, честно говоря, этот код будет работать, он выглядит чистым и сделает большинство людей счастливыми.

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

Другой вариант - использовать AtomicRefrence.

public static AtomicReference<String> shared = new AtomicReference<>();
String init="Inital Value";
shared.set(init);
//now we will modify that value
boolean success=false;
while(!success){
  String prevValue=shared.get();
  // do all the work you need to
  String newValue=shared.get()+"lets add something";
  // Compare and set
  success=shared.compareAndSet(prevValue,newValue);
}

Теперь, почему это лучше? Честно говоря, этот код немного менее чист, чем раньше. Но есть что-то действительно важное, что происходит под капотом в AtomicRefrence, это сравнение и обмен. Это одна команда процессора, а не вызов ОС, которая делает переключение возможным. Это одна инструкция на процессоре. А поскольку нет блокировок, в случае использования блокировки нет переключения контекста, что экономит еще больше времени!

Уловка в том, что для AtomicReferences здесь не используется вызов .equals (), а вместо этого == сравнение ожидаемого значения. Поэтому убедитесь, что ожидаемый фактический объект возвращен из цикла get.

Эрик Хеллерен
источник
14
Ваши два примера ведут себя по-разному. Вы должны были бы зацикливаться, workedчтобы получить ту же семантику.
CurtainDog
5
Я думаю, что вы должны инициализировать значение внутри конструктора AtomicReference, в противном случае другой поток все еще может увидеть значение null, прежде чем вы вызовите shared.set. (Если файл shared.set не запущен в статическом инициализаторе.)
Хенно Вермёлен
8
Во втором примере с Java 8 вы должны использовать что-то вроде: shared.updateAndGet ((x) -> (x + «позволяет что-то добавить»)); ... который будет неоднократно вызывать .compareAndSet, пока он не заработает. Это эквивалентно синхронизированному блоку, который всегда будет успешным. Вы должны убедиться, что лямбда, которую вы проходите, не имеет побочных эффектов, потому что она может вызываться несколько раз.
Том Диббл
2
Нет необходимости делать переменную String sharedValue. Синхронизированный (блокировка) достаточно хорош, чтобы установить, что произошло до отношений.
Джай Пандит
2
«... изменить состояние неизменяемого объекта» здесь неточно, отчасти потому, что будучи буквальным, вы не можете изменить состояние неизменяемого объекта. В этом примере демонстрируется изменение ссылки с одного экземпляра неизменного объекта на другой. Я понимаю, что это педантично, но я думаю, что стоит подчеркнуть, насколько запутанной может быть логика Thread.
Марк Филлипс
30

Вот пример использования для AtomicReference:

Рассмотрим этот класс, который действует как диапазон чисел и использует отдельные переменные AtmomicInteger для поддержания нижних и верхних границ чисел.

public class NumberRange {
    // INVARIANT: lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

    public void setLower(int i) {
        // Warning -- unsafe check-then-act
        if (i > upper.get())
            throw new IllegalArgumentException(
                    "can't set lower to " + i + " > upper");
        lower.set(i);
    }

    public void setUpper(int i) {
        // Warning -- unsafe check-then-act
        if (i < lower.get())
            throw new IllegalArgumentException(
                    "can't set upper to " + i + " < lower");
        upper.set(i);
    }

    public boolean isInRange(int i) {
        return (i >= lower.get() && i <= upper.get());
    }
}

И setLower, и setUpper являются последовательностями проверки-затем-действия, но они не используют достаточную блокировку, чтобы сделать их атомарными. Если диапазон номеров содержит (0, 10), и один поток вызывает setLower (5), а другой поток вызывает setUpper (4), то с некоторым неудачным временем оба пройдут проверки в установщиках, и обе модификации будут применены. В результате диапазон теперь содержит (5, 4) недопустимое состояние. Таким образом, хотя базовые AtomicIntegers являются поточно-ориентированными, составной класс - нет. Это можно исправить с помощью AtomicReference вместо использования отдельных AtomicIntegers для верхних и нижних границ.

public class CasNumberRange {
    // Immutable
    private static class IntPair {
        final int lower;  // Invariant: lower <= upper
        final int upper;

        private IntPair(int lower, int upper) {
            this.lower = lower;
            this.upper = upper;
        }
    }

    private final AtomicReference<IntPair> values = 
            new AtomicReference<IntPair>(new IntPair(0, 0));

    public int getLower() {
        return values.get().lower;
    }

    public void setLower(int lower) {
        while (true) {
            IntPair oldv = values.get();
            if (lower > oldv.upper)
                throw new IllegalArgumentException(
                    "Can't set lower to " + lower + " > upper");
            IntPair newv = new IntPair(lower, oldv.upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }

    public int getUpper() {
        return values.get().upper;
    }

    public void setUpper(int upper) {
        while (true) {
            IntPair oldv = values.get();
            if (upper < oldv.lower)
                throw new IllegalArgumentException(
                    "Can't set upper to " + upper + " < lower");
            IntPair newv = new IntPair(oldv.lower, upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }
}
Бинита Бхарати
источник
2
Эта статья похожа на ваш ответ, но углубляется в более сложные вещи. Это интересно! ibm.com/developerworks/java/library/j-jtp04186
LppEdd
20

Вы можете использовать AtomicReference при применении оптимистических блокировок. У вас есть общий объект, и вы хотите изменить его из более чем одного потока.

  1. Вы можете создать копию общего объекта
  2. Изменить общий объект
  3. Вам нужно проверить, что общий объект все тот же, что и раньше, - если да, то обновите его, указав ссылку на измененную копию.

Поскольку другой поток мог изменить его и / может изменить между этими 2 шагами. Вы должны сделать это в атомарной операции. это где AtomicReference может помочь

HamoriZ
источник
7

Вот очень простой пример использования, который не имеет ничего общего с безопасностью потоков.

Чтобы разделить объект между лямбда-вызовами, AtomicReferenceесть опция :

public void doSomethingUsingLambdas() {

    AtomicReference<YourObject> yourObjectRef = new AtomicReference<>();

    soSomethingThatTakesALambda(() -> {
        yourObjectRef.set(youObject);
    });

    soSomethingElseThatTakesALambda(() -> {
        YourObject yourObject = yourObjectRef.get();
    });
}

Я не говорю, что это хороший дизайн или что-то в этом роде (это просто тривиальный пример), но если у вас есть случай, когда вам нужно разделить объект между лямбда-вызовами, AtomicReference опция является опцией.

Фактически вы можете использовать любой объект, который содержит ссылку, даже коллекцию, которая имеет только один элемент. Тем не менее, AtomicReference идеально подходит.

Бенни Боттема
источник
6

Я не буду много говорить. Мои уважаемые коллеги уже внесли свой ценный вклад.Полноценный исполняемый код в конце этого блога должен устранить любую путаницу. Речь идет о небольшой программе бронирования мест в кино по многопоточному сценарию.

Некоторые важные элементарные факты заключаются в следующем. 1> Различные потоки могут бороться только за экземплярные и статические переменные-члены в пространстве кучи. 2> Volatile чтение или запись полностью атомарны и сериализуются / происходят раньше и делаются только из памяти. Говоря это, я имею в виду, что любое чтение будет следовать за предыдущей записью в памяти. И любая запись будет следовать за предыдущим чтением из памяти. Таким образом, любой поток, работающий с volatile, всегда будет видеть самое актуальное значение. AtomicReference использует это свойство volatile.

Ниже приведены некоторые исходные коды AtomicReference. AtomicReference ссылается на ссылку на объект. Эта ссылка является переменной типа volatile в экземпляре AtomicReference, как показано ниже.

private volatile V value;

get () просто возвращает последнее значение переменной (как это происходит с волатильным способом «происходит раньше»).

public final V get()

Ниже приводится наиболее важный метод AtomicReference.

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

Метод compareAndSet (ожидание, обновление) вызывает метод compareAndSwapObject () небезопасного класса Java. Этот вызов метода unsafe вызывает собственный вызов, который вызывает единственную инструкцию для процессора. «ожидать» и «обновлять» каждая ссылка на объект.

В том и только в том случае, если переменная-член экземпляра AtomicReference «значение» ссылается на тот же объект, на который ссылается «ожидаемо», «обновление» теперь назначается этой переменной экземпляра, и возвращается «истина». Или же ложь возвращается. Все это сделано атомарно. Никакой другой поток не может перехватить между ними. Поскольку это однопроцессорная операция (магия современной компьютерной архитектуры), она часто быстрее, чем использование синхронизированного блока. Но помните, что когда несколько переменных необходимо обновить атомарно, AtomicReference не поможет.

Я хотел бы добавить полноценный работающий код, который можно запустить в Eclipse. Это очистило бы много беспорядка. Здесь 22 пользователя (темы MyTh) пытаются забронировать 20 мест. Ниже приведен фрагмент кода с последующим полным кодом.

Фрагмент кода, где 22 пользователя пытаются забронировать 20 мест.

for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }

Ниже приводится полный код.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class Solution {

    static List<AtomicReference<Integer>> seats;// Movie seats numbered as per
                                                // list index

    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        seats = new ArrayList<>();
        for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }
        for (Thread t : ths) {
            t.join();
        }
        for (AtomicReference<Integer> seat : seats) {
            System.out.print(" " + seat.get());
        }
    }

    /**
     * id is the id of the user
     * 
     * @author sankbane
     *
     */
    static class MyTh extends Thread {// each thread is a user
        static AtomicInteger full = new AtomicInteger(0);
        List<AtomicReference<Integer>> l;//seats
        int id;//id of the users
        int seats;

        public MyTh(List<AtomicReference<Integer>> list, int userId) {
            l = list;
            this.id = userId;
            seats = list.size();
        }

        @Override
        public void run() {
            boolean reserved = false;
            try {
                while (!reserved && full.get() < seats) {
                    Thread.sleep(50);
                    int r = ThreadLocalRandom.current().nextInt(0, seats);// excludes
                                                                            // seats
                                                                            //
                    AtomicReference<Integer> el = l.get(r);
                    reserved = el.compareAndSet(null, id);// null means no user
                                                            // has reserved this
                                                            // seat
                    if (reserved)
                        full.getAndIncrement();
                }
                if (!reserved && full.get() == seats)
                    System.out.println("user " + id + " did not get a seat");
            } catch (InterruptedException ie) {
                // log it
            }
        }
    }

}    
Санкар Банерджи
источник
5

Когда мы используем AtomicReference?

AtomicReference - это гибкий способ атомарного обновления значения переменной без использования синхронизации.

AtomicReference поддержка безпотокового программирования без блокировок для отдельных переменных.

Существует несколько способов достижения безопасности потоков с помощью высокоуровневого параллельного API. Атомарные переменные - это один из нескольких вариантов.

Lock Объекты поддерживают идиомы блокировки, которые упрощают многие параллельные приложения.

Executorsопределить высокоуровневый API для запуска и управления потоками. Реализации исполнителя, предоставляемые java.util.concurrent, обеспечивают управление пулом потоков, подходящее для крупномасштабных приложений.

Параллельные коллекции облегчают управление большими коллекциями данных и могут значительно снизить потребность в синхронизации.

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

Приведите простой пример использования AtomicReference.

Пример кода с AtomicReference:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

Нужно ли создавать объекты во всех многопоточных программах?

Вам не нужно использовать AtomicReference во всех многопоточных программах.

Если вы хотите защитить одну переменную, используйте AtomicReference. Если вы хотите защитить блок кода, используйте другие конструкции, такие как Lock/ synchronizedи т. Д.

Равиндра Бабу
источник
-1

Другой простой пример - модификация безопасного потока в объекте сеанса.

public PlayerScore getHighScore() {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    return holder.get();
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    while (true) {
        HighScore old = holder.get();
        if (old.score >= newScore.score)
            break;
        else if (holder.compareAndSet(old, newScore))
            break;
    } 
}

Источник: http://www.ibm.com/developerworks/library/j-jtp09238/index.html

Dherik
источник