Kiedy używać AtomicReference w Javie?


311

Kiedy korzystamy AtomicReference?

Czy konieczne jest tworzenie obiektów we wszystkich programach wielowątkowych?

Podaj prosty przykład, w którym należy użyć AtomicReference.

Odpowiedzi:


215

Odniesienia atomowego należy używać w ustawieniu, w którym należy wykonywać proste operacje atomowe (tj. Bezpieczne dla wątków , nietrywialne) na odwołaniu, dla których synchronizacja na podstawie monitorowania nie jest odpowiednia. Załóżmy, że chcesz sprawdzić, czy określone pole jest wyświetlane tylko wtedy, gdy stan obiektu pozostaje taki, jak podczas ostatniego sprawdzania:

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);

Ze względu na semantykę odniesień atomowych możesz to zrobić, nawet jeśli cacheobiekt jest współużytkowany przez wątki, bez użycia synchronized. Zasadniczo lepiej jest używać synchronizatorów lub java.util.concurrentframeworka niż goły, Atomic*chyba że wiesz, co robisz.

Dwie doskonałe referencje dotyczące martwych drzew, które wprowadzą Cię w ten temat:

Zauważ, że (nie wiem, czy to zawsze było prawdą) przypisanie referencji (tj. =) Samo w sobie jest atomowe (aktualizowanie prymitywnych 64-bitowych typów takich jak longlub doublenie może być atomowe; ale aktualizacja referencji jest zawsze atomowa, nawet jeśli jest 64-bitowa ) bez jawnego użycia Atomic*.
Patrz specyfikacja języka Java 3ed, rozdział 17.7 .


43
Popraw mnie, jeśli się mylę, ale wydaje się, że kluczem do tego jest to, że musisz zrobić „CompareAndSet”. Gdyby wszystko, co musiałem zrobić, nie było w ogóle potrzebne, by AtomicObject nie był potrzebny, ponieważ same aktualizacje referencyjne są atomowe?
sMoZely

Czy bezpieczne jest cache.compareAndSet (cachedValue, someFunctionOfOld (cachedValueToUpdate))? Czyli wstawisz obliczenia?
kaqqao

4
@veggen Argumenty funkcji w Javie są obliczane przed samą funkcją, więc wstawianie nie ma znaczenia w tym przypadku. Tak, jest bezpieczny.
Dmitry

29
@sMoZely To prawda, ale jeśli nie używasz AtomicReference, powinieneś zaznaczyć zmienną, volatileponieważ chociaż środowisko wykonawcze gwarantuje, że przypisanie referencji jest niepodzielne, kompilator może przeprowadzić optymalizacje przy założeniu, że zmienna nie była modyfikowana przez inne wątki.
kbolino

1
@BradCupit zauważ, że powiedziałem „jeśli nie używasz AtomicReference”; jeśli przy użyciu go, a następnie moja rada to iść w kierunku przeciwnym i oznaczyć go finaltak kompilator może odpowiednio zoptymalizować.
kbolino

91

Odwołanie atomowe jest idealne do użycia, gdy trzeba udostępnić i zmienić stan niezmiennego obiektu między wieloma wątkami. To bardzo gęste stwierdzenie, więc trochę je podzielę.

Po pierwsze, niezmienny obiekt to obiekt, który nie ulega zmianie po budowie. Często metody niezmiennego obiektu zwracają nowe wystąpienia tej samej klasy. Niektóre przykłady obejmują klasy opakowań Long i Double, a także String, żeby wymienić tylko kilka. (Według Programowania współbieżności na niezmiennych obiektach JVM są krytyczne elementy współczesnej współbieżności).

Następnie, dlaczego AtomicReference jest lepszy niż zmienny obiekt do dzielenia się tą wspólną wartością. Prosty przykład kodu pokaże różnicę.

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

Za każdym razem, gdy chcesz zmodyfikować ciąg, do którego odwołuje się to zmienne pole w oparciu o jego bieżącą wartość, najpierw musisz uzyskać blokadę tego obiektu. Zapobiega to pojawianiu się w międzyczasie jakiegoś innego wątku i zmianie wartości w środku nowego łączenia łańcuchów. Następnie, gdy wznowisz wątek, zablokujesz pracę drugiego wątku. Ale szczerze mówiąc, ten kod będzie działał, wygląda na czysty i uszczęśliwiłby większość ludzi.

Niewielki problem. To jest wolne. Zwłaszcza jeśli istnieje spory o obiekt blokady. Dzieje się tak, ponieważ większość blokad wymaga wywołania systemowego OS, a Twój wątek blokuje się i zostaje wyłączony kontekstowo z procesora, aby zrobić miejsce dla innych procesów.

Inną opcją jest użycie 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);
}

Dlaczego to jest lepsze? Szczerze mówiąc, ten kod jest trochę mniej czysty niż wcześniej. Ale w AtomicRefrence dzieje się coś naprawdę ważnego, to jest porównywanie i zamiana. Jest to instrukcja pojedynczego procesora, a nie wywołanie systemu operacyjnego, które powoduje zmianę. To jest pojedyncza instrukcja dotycząca procesora. A ponieważ nie ma blokad, nie ma przełącznika kontekstu w przypadku blokowania, co oszczędza jeszcze więcej czasu!

W przypadku AtomicReferences haczykiem nie jest wywołanie .equals (), lecz porównanie == oczekiwanej wartości. Upewnij się więc, że oczekiwany jest rzeczywisty obiekt zwracany z pętli get.


14
Twoje dwa przykłady zachowują się inaczej. Będziesz musiał zapętlić się, workedaby uzyskać tę samą semantykę.
CurtainDog

5
Myślę, że powinieneś zainicjować wartość wewnątrz konstruktora AtomicReference, w przeciwnym razie inny wątek może nadal widzieć wartość null przed wywołaniem shared.set. (Chyba, że ​​shared.set jest uruchamiany w statycznym inicjalizatorze).
Henno Vermeulen

8
W drugim przykładzie powinieneś od Java 8 użyć czegoś takiego: shared.updateAndGet ((x) -> (x + „pozwala dodać coś”)); ... który będzie wywoływał .compareAndSet, dopóki nie zadziała. Jest to równoważne zsynchronizowanemu blokowi, który zawsze się powiedzie. Musisz upewnić się, że przekazywana lambda jest wolna od skutków ubocznych, ponieważ może być wywoływana wiele razy.
Tom Dibble,

2
Nie trzeba tworzyć zmiennego ciągu String sharedValue. Zsynchronizowany (blokada) jest wystarczająco dobry, aby ustalić zdarzenie przed relacją.
Jai Pandit,

2
„... zmiana stanu obiektu niezmiennego” jest tutaj nieprecyzyjna, po części dlatego, że będąc dosłownie nie można zmienić stanu niezmiennego obiektu. Przykład pokazuje zmianę odwołania z jednej niezmiennej instancji obiektu na inną. Zdaję sobie sprawę, że to pedantyczne, ale myślę, że warto to podkreślić, biorąc pod uwagę, jak myląca może być logika wątków.
Mark Phillips

30

Oto przypadek użycia AtomicReference:

Rozważ tę klasę, która działa jako zakres liczb i wykorzystuje indywidualne zmienne AtmomicInteger do utrzymania dolnej i górnej granicy liczb.

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());
    }
}

Zarówno setLower, jak i setUpper są sekwencjami sprawdzającymi, a następnie działającymi, ale nie używają wystarczającego blokowania, aby uczynić je atomowymi. Jeśli zakres liczb zawiera (0, 10), a jeden wątek wywołuje setLower (5), podczas gdy inny wątek wywołuje setUpper (4), przy pewnym niefortunnym taktowaniu oba przejdą testy w seterach i zostaną zastosowane obie modyfikacje. W rezultacie zakres ma teraz (5, 4) nieprawidłowy stan. Tak więc, podczas gdy leżące u podstaw AtomicIntegers są bezpieczne dla wątków, klasa złożona nie jest. Można to naprawić za pomocą AtomicReference zamiast pojedynczych AtomicInteger dla górnej i dolnej granicy.

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
Ten artykuł jest podobny do twojej odpowiedzi, ale zagłębia się w bardziej skomplikowane rzeczy. To interesujące! ibm.com/developerworks/java/library/j-jtp04186
LppEdd

20

Możesz zastosować AtomicReference podczas stosowania optymistycznych blokad. Masz obiekt współdzielony i chcesz go zmienić z więcej niż jednego wątku.

  1. Możesz utworzyć kopię współdzielonego obiektu
  2. Zmodyfikuj obiekt współdzielony
  3. Musisz sprawdzić, czy współdzielony obiekt jest nadal taki sam jak poprzednio - jeśli tak, zaktualizuj go, odwołując się do zmodyfikowanej kopii.

Ponieważ inny wątek mógł go zmodyfikować i / lub może modyfikować między tymi 2 krokami. Musisz to zrobić w operacji atomowej. tutaj może pomóc AtomicReference


7

Oto bardzo prosty przypadek użycia i nie ma nic wspólnego z bezpieczeństwem wątków.

Aby udostępnić obiekt między wywołaniami lambda, dostępna AtomicReferencejest opcja :

public void doSomethingUsingLambdas() {

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

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

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

Nie twierdzę, że jest to dobry projekt lub cokolwiek (to tylko trywialny przykład), ale jeśli masz przypadek, w którym musisz współdzielić obiekt między wywołaniami lambda, AtomicReferencejest to opcja.

W rzeczywistości możesz użyć dowolnego obiektu zawierającego odniesienie, nawet kolekcji zawierającej tylko jeden element. Jednak AtomicReference jest idealnie dopasowany.


6

Nie będę dużo rozmawiać. Już moi szanowani koledzy wnieśli swój cenny wkład. Pełny działający kod na końcu tego bloga powinien usunąć wszelkie nieporozumienia. Chodzi o rezerwację miejsca filmowego w małym programie w scenariuszu wielowątkowym.

Niektóre ważne elementarne fakty są następujące. 1> Różne wątki mogą rywalizować tylko na przykład o statyczne zmienne składowe w przestrzeni sterty. 2> Lotne odczytywanie lub zapisywanie jest całkowicie atomowe i szeregowane / odbywa się wcześniej i odbywa się wyłącznie z pamięci. Mówiąc to, mam na myśli, że każdy odczyt nastąpi po poprzednim zapisie w pamięci. I każdy zapis będzie następował po poprzednim odczycie z pamięci. Tak więc każdy wątek pracujący z niestabilną zawsze będzie widział najbardziej aktualną wartość. AtomicReference używa tej właściwości lotnej.

Oto niektóre z kodu źródłowego AtomicReference. AtomicReference odnosi się do odwołania do obiektu. To odwołanie jest zmienną zmienną składową w instancji AtomicReference, jak poniżej.

private volatile V value;

get () zwraca po prostu ostatnią wartość zmiennej (tak jak substancje lotne robią to w „zdarzeniu przed”).

public final V get()

Oto najważniejsza metoda AtomicReference.

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

Metoda CompareAndSet (expect, update) wywołuje metodę CompareAndSwapObject () niebezpiecznej klasy Java. Ta metoda wywołania niebezpiecznego wywołuje wywołanie rodzime, które wywołuje pojedynczą instrukcję do procesora. „expect” i „update” odnoszą się do obiektu.

Jeśli i tylko wtedy, gdy zmienna elementu „instancja” AtomicReference odnosi się do tego samego obiektu jest określana przez „oczekiwanie”, „aktualizacja” jest teraz przypisywana do tej zmiennej instancji i zwracane jest „prawda”. W przeciwnym razie zwracana jest wartość false. Wszystko odbywa się atomowo. Żaden inny wątek nie może przechodzić między nimi. Ponieważ jest to operacja jednoprocesorowa (magia współczesnej architektury komputerowej), często jest szybsza niż użycie bloku synchronicznego. Pamiętaj jednak, że gdy wiele zmiennych wymaga aktualizacji atomowej, AtomicReference nie pomoże.

Chciałbym dodać pełny działający kod, który można uruchomić w środowisku Eclipse. Rozwiałoby to wiele zamieszania. Tutaj 22 użytkowników (wątki MyTh) próbuje zarezerwować 20 miejsc. Poniżej znajduje się fragment kodu, po którym następuje pełny kod.

Fragment kodu, w którym 22 użytkowników próbuje zarezerwować 20 miejsc.

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();
        }

Poniżej znajduje się pełny działający kod.

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

Kiedy używamy AtomicReference?

AtomicReference to elastyczny sposób na atomową aktualizację wartości zmiennej bez synchronizacji.

AtomicReference obsługuje bezpieczne dla wątków programowanie na pojedynczych zmiennych.

Istnieje wiele sposobów osiągnięcia bezpieczeństwa wątków dzięki współbieżnemu interfejsowi API wysokiego poziomu . Zmienne atomowe to jedna z wielu opcji.

Lock obiekty obsługują idiomy blokujące, które upraszczają wiele jednoczesnych aplikacji.

Executorszdefiniuj interfejs API wysokiego poziomu do uruchamiania wątków i zarządzania nimi. Implementacje executorów dostarczone przez java.util.concurrent zapewniają zarządzanie pulą wątków odpowiednie dla aplikacji na dużą skalę.

Współbieżne kolekcje ułatwiają zarządzanie dużymi kolekcjami danych i mogą znacznie zmniejszyć potrzebę synchronizacji.

Zmienne atomowe mają funkcje minimalizujące synchronizację i pomagające uniknąć błędów spójności pamięci.

Podaj prosty przykład, w którym należy użyć AtomicReference.

Przykładowy kod z 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);

Czy konieczne jest tworzenie obiektów we wszystkich programach wielowątkowych?

Nie musisz używać AtomicReferencewe wszystkich programach wielowątkowych.

Jeśli chcesz chronić pojedynczą zmienną, użyj AtomicReference. Jeśli chcesz strzec bloku kodu, użyj innych konstrukcji, takich jak Lock/ synchronizeditp.


-1

Innym prostym przykładem jest modyfikacja bezpiecznego wątku w obiekcie sesji.

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;
    } 
}

Źródło: http://www.ibm.com/developerworks/library/j-jtp09238/index.html

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.