Wann wird AtomicReference in Java verwendet?

311

Wann verwenden wir AtomicReference?

Müssen Objekte in allen Multithread-Programmen erstellt werden?

Geben Sie ein einfaches Beispiel an, in dem AtomicReference verwendet werden soll.

Chintu
quelle

Antworten:

215

Die atomare Referenz sollte in einer Umgebung verwendet werden, in der Sie einfache atomare (dh threadsichere , nicht triviale) Operationen an einer Referenz ausführen müssen, für die eine überwacherbasierte Synchronisation nicht geeignet ist. Angenommen, Sie möchten nur dann überprüfen, ob ein bestimmtes Feld vorhanden ist, wenn der Status des Objekts so bleibt, wie Sie es zuletzt überprüft haben:

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

Aufgrund der atomaren Referenzsemantik können Sie dies auch dann tun, wenn das cacheObjekt von Threads gemeinsam genutzt wird, ohne es zu verwenden synchronized. Im Allgemeinen ist es besser, Synchronizer oder das java.util.concurrentFramework zu verwenden, als nackt zu sein, es Atomic*sei denn, Sie wissen, was Sie tun.

Zwei ausgezeichnete Dead-Tree-Referenzen, die Sie in dieses Thema einführen:

Beachten Sie, dass die Referenzzuweisung (dh ich weiß nicht, ob dies immer der Fall war) =selbst atomar ist (Aktualisieren primitiver 64-Bit-Typen wie longoder doublemöglicherweise nicht atomar ist; das Aktualisieren einer Referenz jedoch immer atomar ist, selbst wenn es 64-Bit ist ) ohne explizite Verwendung eines Atomic*.
Siehe die Java-Sprachspezifikation 3ed, Abschnitt 17.7 .

andersoj
quelle
43
Korrigieren Sie mich, wenn ich falsch liege, aber es scheint, dass der Schlüssel dazu darin besteht, dass Sie ein "compareAndSet" durchführen müssen. Wenn ich nur festlegen müsste, würde ich das AtomicObject überhaupt nicht benötigen, da Referenzaktualisierungen selbst atomar sind?
sMoZely
Ist es sicher, cache.compareAndSet (cachedValue, someFunctionOfOld (cachedValueToUpdate)) auszuführen? Dh Inline die Berechnung?
Kaqqao
4
@veggen Funktionsargumente in Java werden vor der Funktion selbst ausgewertet, sodass Inlining in diesem Fall keinen Unterschied macht. Ja, es ist sicher.
Dmitry
29
@sMoZely Das ist richtig, aber wenn Sie es nicht verwenden AtomicReference, sollten Sie die Variable markieren, volatileda die Laufzeit zwar garantiert, dass die Referenzzuweisung atomar ist, der Compiler jedoch Optimierungen unter der Annahme durchführen kann, dass die Variable nicht von anderen Threads geändert wurde.
Kbolino
1
@BradCupit beachten Sie, dass ich sagte "wenn Sie nicht verwenden AtomicReference"; Wenn Sie es verwenden, würde ich raten, in die entgegengesetzte Richtung zu gehen und es zu markieren, finaldamit der Compiler entsprechend optimieren kann.
Kbolino
91

Eine atomare Referenz ist ideal, wenn Sie den Status eines unveränderlichen Objekts zwischen mehreren Threads teilen und ändern müssen. Das ist eine super dichte Aussage, also werde ich sie ein bisschen aufschlüsseln.

Erstens ist ein unveränderliches Objekt ein Objekt, das nach der Konstruktion effektiv nicht verändert wird. Häufig geben die Methoden eines unveränderlichen Objekts neue Instanzen derselben Klasse zurück. Einige Beispiele sind die Wrapper-Klassen Long und Double sowie String, um nur einige zu nennen. (Gemäß Programming Concurrency in der JVM unveränderliche Objekte ein kritischer Bestandteil der modernen Parallelität).

Warum ist AtomicReference besser als ein flüchtiges Objekt, um diesen gemeinsamen Wert zu teilen? Ein einfaches Codebeispiel zeigt den Unterschied.

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

Jedes Mal, wenn Sie die Zeichenfolge, auf die dieses flüchtige Feld verweist, basierend auf dem aktuellen Wert ändern möchten, müssen Sie zuerst eine Sperre für dieses Objekt erhalten. Dadurch wird verhindert, dass in der Zwischenzeit ein anderer Thread eingeht und der Wert in der Mitte der neuen Zeichenfolgenverkettung geändert wird. Wenn Ihr Thread fortgesetzt wird, blockieren Sie die Arbeit des anderen Threads. Aber ehrlich gesagt wird dieser Code funktionieren, er sieht sauber aus und würde die meisten Menschen glücklich machen.

Kleines Problem. Es ist langsam. Vor allem, wenn es viele Konflikte mit diesem Sperrobjekt gibt. Dies liegt daran, dass die meisten Sperren einen Systemaufruf des Betriebssystems erfordern und Ihr Thread blockiert und aus der CPU kontextgeschaltet wird, um Platz für andere Prozesse zu schaffen.

Die andere Option ist die Verwendung einer 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);
}

Warum ist das besser? Ehrlich gesagt ist dieser Code etwas weniger sauber als zuvor. Aber in AtomicRefrence passiert etwas wirklich Wichtiges unter der Haube, nämlich Vergleichen und Tauschen. Es ist ein einzelner CPU-Befehl, kein Betriebssystemaufruf, der den Wechsel ermöglicht. Das ist eine einzelne Anweisung auf der CPU. Und weil es keine Sperren gibt, gibt es keinen Kontextwechsel für den Fall, dass die Sperre ausgeübt wird, was noch mehr Zeit spart!

Der Haken ist, dass für AtomicReferences kein .equals () -Aufruf verwendet wird, sondern ein == -Vergleich für den erwarteten Wert. Stellen Sie also sicher, dass das erwartete Objekt das tatsächliche Objekt ist, das von get in the loop zurückgegeben wird.

Erik Helleren
quelle
14
Ihre beiden Beispiele verhalten sich unterschiedlich. Sie müssten eine Schleife machenworked , um die gleiche Semantik zu erhalten.
CurtainDog
5
Ich denke, Sie sollten den Wert im AtomicReference-Konstruktor initialisieren, da sonst in einem anderen Thread der Wert möglicherweise noch null angezeigt wird, bevor Sie shared.set aufrufen. (Es sei denn, shared.set wird in einem statischen Initialisierer ausgeführt.)
Henno Vermeulen
8
In Ihrem zweiten Beispiel sollten Sie ab Java 8 Folgendes verwenden: shared.updateAndGet ((x) -> (x + "lässt uns etwas hinzufügen")); ... das wiederholt .compareAndSet aufruft, bis es funktioniert. Das entspricht dem synchronisierten Block, der immer erfolgreich wäre. Sie müssen jedoch sicherstellen, dass das Lambda, das Sie abgeben, nebenwirkungsfrei ist, da es möglicherweise mehrmals aufgerufen wird.
Tom Dibble
2
Es ist nicht erforderlich, einen flüchtigen String sharedValue zu erstellen. Die Synchronisierung (Sperre) ist gut genug, um das Geschehen vor der Beziehung festzustellen.
Jai Pandit
2
"... den Status eines unveränderlichen Objekts ändern" ist hier ungenau, zum Teil, weil Sie als Literal den Status eines unveränderlichen Objekts nicht ändern können. Das Beispiel zeigt, wie die Referenz von einer unveränderlichen Objektinstanz in eine andere geändert wird. Mir ist klar, dass das pedantisch ist, aber ich denke, es lohnt sich hervorzuheben, wie verwirrend die Thread-Logik sein kann.
Mark Phillips
30

Hier ist ein Anwendungsfall für AtomicReference:

Betrachten Sie diese Klasse, die als Zahlenbereich fungiert und einzelne AtmomicInteger-Variablen verwendet, um die unteren und oberen Zahlengrenzen beizubehalten.

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

Sowohl setLower als auch setUpper sind Check-Then-Act-Sequenzen, verwenden jedoch nicht genügend Sperren, um sie atomar zu machen. Wenn der Nummernkreis gilt (0, 10) und ein Thread setLower (5) aufruft, während ein anderer Thread setUpper (4) aufruft, bestehen beide mit etwas unglücklichem Timing die Prüfungen in den Setzern und beide Modifikationen werden angewendet. Das Ergebnis ist, dass der Bereich jetzt (5, 4) einen ungültigen Zustand enthält. Während die zugrunde liegenden AtomicIntegers threadsicher sind, ist dies bei der zusammengesetzten Klasse nicht der Fall. Dies kann mithilfe einer AtomicReference behoben werden, anstatt einzelne AtomicIntegers für die oberen und unteren Grenzen zu verwenden.

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;
        }
    }
}
Binita Bharati
quelle
2
Dieser Artikel ähnelt Ihrer Antwort, geht jedoch auf kompliziertere Dinge ein. Es ist interessant! ibm.com/developerworks/java/library/j-jtp04186
LppEdd
20

Sie können AtomicReference verwenden, wenn Sie optimistische Sperren anwenden. Sie haben ein freigegebenes Objekt und möchten es von mehr als einem Thread ändern.

  1. Sie können eine Kopie des freigegebenen Objekts erstellen
  2. Ändern Sie das freigegebene Objekt
  3. Sie müssen überprüfen, ob das freigegebene Objekt noch dasselbe ist wie zuvor. Wenn ja, aktualisieren Sie es mit der Referenz der geänderten Kopie.

Da andere Thread es möglicherweise geändert haben und / oder zwischen diesen beiden Schritten ändern können. Sie müssen es in einer atomaren Operation tun. Hier kann AtomicReference helfen

HamoriZ
quelle
7

Hier ist ein sehr einfacher Anwendungsfall, der nichts mit der Gewindesicherheit zu tun hat.

Um ein Objekt zwischen Lambda-Aufrufen zu teilen, AtomicReferenceist dies eine Option :

public void doSomethingUsingLambdas() {

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

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

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

Ich sage nicht, dass dies gutes Design ist oder so (es ist nur ein triviales Beispiel), aber wenn Sie den Fall haben, dass Sie ein Objekt zwischen Lambda-Aufrufen teilen müssen, ist das AtomicReference ist dies eine Option.

Tatsächlich können Sie jedes Objekt verwenden, das eine Referenz enthält, auch eine Sammlung mit nur einem Element. Die AtomicReference passt jedoch perfekt.

Benny Bottema
quelle
6

Ich werde nicht viel reden. Meine angesehenen Freunde haben bereits wertvolle Beiträge geleistet. Der vollwertige Laufcode am Ende dieses Blogs sollte jegliche Verwirrung beseitigen. Es geht um ein kleines Programm für die Buchung eines Kinositzes in einem Multithread-Szenario.

Einige wichtige elementare Tatsachen sind wie folgt. 1> Verschiedene Threads können nur mit statischen Elementvariablen im Heap-Bereich konkurrieren. 2> Flüchtiges Lesen oder Schreiben ist vollständig atomar und serialisiert / geschieht vorher und erfolgt nur aus dem Speicher. Damit meine ich, dass jeder Lesevorgang dem vorherigen Schreibvorgang im Speicher folgt. Und jedes Schreiben folgt dem vorherigen Lesen aus dem Speicher. Jeder Thread, der mit einem flüchtigen Thread arbeitet, sieht also immer den aktuellsten Wert. AtomicReference verwendet diese Eigenschaft von flüchtig.

Im Folgenden finden Sie einige Quellcodes von AtomicReference. AtomicReference bezieht sich auf eine Objektreferenz. Diese Referenz ist eine flüchtige Mitgliedsvariable in der AtomicReference-Instanz (siehe unten).

private volatile V value;

get () gibt einfach den neuesten Wert der Variablen zurück (wie es bei flüchtigen Stoffen der Fall ist).

public final V get()

Das Folgende ist die wichtigste Methode von AtomicReference.

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

Die Methode compareAndSet (erwarten, aktualisieren) ruft die Methode compareAndSwapObject () der unsicheren Java-Klasse auf. Dieser unsichere Methodenaufruf ruft den nativen Aufruf auf, der eine einzelne Anweisung an den Prozessor aufruft. "Erwarten" und "Aktualisieren" jeder Referenz eines Objekts.

Wenn und nur wenn die AtomicReference-Instanzmitgliedsvariable "value" auf dasselbe Objekt verweist, wird auf "expected" verwiesen, "update" wird jetzt dieser Instanzvariablen zugewiesen und "true" wird zurückgegeben. Andernfalls wird false zurückgegeben. Das Ganze wird atomar gemacht. Kein anderer Thread kann dazwischen abfangen. Da es sich um eine Einzelprozessoroperation handelt (Magie der modernen Computerarchitektur), ist sie häufig schneller als die Verwendung eines synchronisierten Blocks. Aber denken Sie daran , dass , wenn mehrere Variablen müssen atomar aktualisiert werden, AtomicReference wird nicht helfen.

Ich möchte einen vollwertigen laufenden Code hinzufügen, der in Eclipse ausgeführt werden kann. Es würde viele Verwirrung beseitigen. Hier versuchen 22 Benutzer (MyTh-Threads) 20 Plätze zu buchen. Es folgt das Code-Snippet, gefolgt vom vollständigen Code.

Code-Snippet, bei dem 22 Benutzer versuchen, 20 Plätze zu buchen.

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

Es folgt der vollständige laufende Code.

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

}    
Sankar Banerjee
quelle
5

Wann verwenden wir AtomicReference?

AtomicReference ist eine flexible Möglichkeit, den Variablenwert ohne Verwendung der Synchronisation atomar zu aktualisieren.

AtomicReference Unterstützung der sperrenfreien threadsicheren Programmierung einzelner Variablen.

Es gibt mehrere Möglichkeiten, die Thread-Sicherheit mit einer gleichzeitigen API auf hoher Ebene zu erreichen . Atomare Variablen sind eine der vielen Optionen.

Lock Objekte unterstützen Sperrsprachen, die viele gleichzeitige Anwendungen vereinfachen.

ExecutorsDefinieren Sie eine allgemeine API zum Starten und Verwalten von Threads. Von java.util.concurrent bereitgestellte Executor-Implementierungen bieten eine Thread-Pool-Verwaltung, die für umfangreiche Anwendungen geeignet ist.

Gleichzeitige Sammlungen erleichtern die Verwaltung großer Datensammlungen und können den Synchronisierungsbedarf erheblich reduzieren.

Atomare Variablen verfügen über Funktionen, die die Synchronisation minimieren und Speicherkonsistenzfehler vermeiden.

Geben Sie ein einfaches Beispiel an, in dem AtomicReference verwendet werden soll.

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

Müssen Objekte in allen Multithread-Programmen erstellt werden?

Sie müssen nicht verwenden AtomicReference in allen Multithread-Programmen verwenden.

Wenn Sie eine einzelne Variable schützen möchten, verwenden Sie AtomicReference. Wenn Sie einen Codeblock schützen möchten, verwenden Sie andere Konstrukte wie Lock/ synchronizedusw.

Ravindra Babu
quelle
-1

Ein weiteres einfaches Beispiel ist eine Safe-Thread-Änderung in einem Sitzungsobjekt.

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

Quelle: http://www.ibm.com/developerworks/library/j-jtp09238/index.html

Dherik
quelle