Praktische Anwendungen für AtomicInteger


228

Ich verstehe irgendwie, dass AtomicInteger und andere Atomic-Variablen gleichzeitige Zugriffe ermöglichen. In welchen Fällen wird diese Klasse normalerweise verwendet?

Antworten:


189

Es gibt zwei Hauptverwendungen von AtomicInteger:

  • Als Atomzähler ( incrementAndGet()usw.), der von vielen Threads gleichzeitig verwendet werden kann

  • Als Grundelement, das den Befehl zum Vergleichen und Austauschen ( compareAndSet()) unterstützt, um nicht blockierende Algorithmen zu implementieren.

    Hier ist ein Beispiel für einen nicht blockierenden Zufallszahlengenerator aus Brian Göetz 'Java Concurrency In Practice :

    public class AtomicPseudoRandom extends PseudoRandom {
        private AtomicInteger seed;
        AtomicPseudoRandom(int seed) {
            this.seed = new AtomicInteger(seed);
        }
    
        public int nextInt(int n) {
            while (true) {
                int s = seed.get();
                int nextSeed = calculateNext(s);
                if (seed.compareAndSet(s, nextSeed)) {
                    int remainder = s % n;
                    return remainder > 0 ? remainder : remainder + n;
                }
            }
        }
        ...
    }
    

    Wie Sie sehen können, funktioniert es im Grunde fast genauso wie incrementAndGet(), führt jedoch eine willkürliche Berechnung ( calculateNext()) anstelle eines Inkrements durch (und verarbeitet das Ergebnis vor der Rückgabe).


1
Ich glaube ich verstehe die erste Verwendung. Dies soll sicherstellen, dass der Zähler inkrementiert wurde, bevor erneut auf ein Attribut zugegriffen wird. Richtig? Könnten Sie ein kurzes Beispiel für die zweite Verwendung geben?
James P.

8
Ihr Verständnis der ersten Verwendung ist irgendwie wahr - es stellt einfach sicher, dass, wenn ein anderer Thread den Zähler zwischen den Operationen readund ändert write that value + 1, dies erkannt wird, anstatt das alte Update zu überschreiben (um das Problem "verlorenes Update" zu vermeiden). Dies ist tatsächlich ein Sonderfall von compareAndSet- wenn der alte Wert war 2, ruft die Klasse tatsächlich auf compareAndSet(2, 3)- wenn also ein anderer Thread den Wert in der Zwischenzeit geändert hat, wird die Inkrementierungsmethode effektiv von Anfang an neu gestartet.
Andrzej Doyle

3
"Rest> 0? Rest: Rest + n;" Gibt es in diesem Ausdruck einen Grund, n einen Rest hinzuzufügen, wenn er 0 ist?
Sandeepkunkunuru

100

Das absolut einfachste Beispiel, das ich mir vorstellen kann, ist das Inkrementieren einer atomaren Operation.

Mit Standard-Ints:

private volatile int counter;

public int getNextUniqueIndex() {
    return counter++; // Not atomic, multiple threads could get the same result
}

Mit AtomicInteger:

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}

Letzteres ist eine sehr einfache Möglichkeit, einfache Mutationseffekte (insbesondere Zählen oder eindeutige Indizierung) durchzuführen, ohne den gesamten Zugriff synchronisieren zu müssen.

Eine komplexere synchronisationsfreie Logik kann verwendet werden, indem compareAndSet()eine Art optimistisches Sperren verwendet wird. Ermitteln Sie den aktuellen Wert, berechnen Sie das Ergebnis basierend darauf, setzen Sie dieses Ergebnis, wenn der Wert immer noch die Eingabe für die Berechnung ist, andernfalls starten Sie erneut Zählbeispiele sind sehr nützlich, und ich verwende sie häufig AtomicIntegerszum Zählen und für VM-weite eindeutige Generatoren, wenn es Hinweise darauf gibt, dass mehrere Threads beteiligt sind, da sie so einfach zu bearbeiten sind. Ich würde es fast als vorzeitige Optimierung betrachten, einfach zu verwenden ints.

Während Sie fast immer die gleichen Synchronisationsgarantien mit intsund entsprechende synchronizedDeklarationen erzielen können , besteht das Schöne daran, AtomicIntegerdass die Thread-Sicherheit in das eigentliche Objekt selbst eingebaut ist, anstatt sich um die möglichen Verschachtelungen und Monitore jeder Methode kümmern zu müssen das passiert, um auf den intWert zuzugreifen . Es ist viel schwieriger, beim Anrufen versehentlich die Thread-Sicherheit zu verletzen, getAndIncrement()als wenn Sie zurückkehren i++und sich daran erinnern (oder nicht), vorher die richtigen Monitore zu erwerben.


2
Vielen Dank für diese klare Erklärung. Was wären die Vorteile einer AtomicInteger gegenüber einer Klasse, in der alle Methoden synchronisiert sind? Würde letzteres als "schwerer" angesehen werden?
James P.

3
Aus meiner Sicht ist es hauptsächlich die Kapselung, die Sie mit AtomicIntegers erhalten - die Synchronisierung erfolgt genau nach Ihren Wünschen, und Sie erhalten beschreibende Methoden in der öffentlichen API, um das beabsichtigte Ergebnis zu erläutern. (Und bis zu einem gewissen Grad haben Sie Recht. Oft werden einfach alle Methoden in einer Klasse synchronisiert, die wahrscheinlich zu grobkörnig ist. Wenn HotSpot jedoch Sperrenoptimierungen und Regeln gegen vorzeitige Optimierung durchführt, halte ich die Lesbarkeit für eine größerer Nutzen als Leistung.)
Andrzej Doyle

Dies ist eine sehr klare und präzise Erklärung. Danke !!
Akash5288

Endlich eine Erklärung, die es für mich richtig geklärt hat.
Benny Bottema

58

Wenn Sie sich die Methoden von AtomicInteger ansehen, werden Sie feststellen, dass sie in der Regel allgemeinen Operationen für Ints entsprechen. Zum Beispiel:

static AtomicInteger i;

// Later, in a thread
int current = i.incrementAndGet();

ist die thread-sichere Version davon:

static int i;

// Later, in a thread
int current = ++i;

Die Methodenzuordnung
++ilautet wie folgt : is i.incrementAndGet()
i++is i.getAndIncrement()
--iis i.decrementAndGet()
i--is i.getAndDecrement()
i = xis i.set(x)
x = iis isx = i.get()

Es gibt auch andere Bequemlichkeitsmethoden wie compareAndSetoderaddAndGet


37

Die Hauptverwendung von AtomicIntegerist, wenn Sie sich in einem Multithread-Kontext befinden und threadsichere Operationen für eine Ganzzahl ohne Verwendung ausführen müssen synchronized. Die Zuweisung und das Abrufen des primitiven Typs intsind bereits atomar, es gibt jedoch AtomicIntegerviele Operationen, die nicht atomar sind int.

Am einfachsten sind die getAndXXXoder xXXAndGet. Zum Beispiel getAndIncrement()ist ein atomares Äquivalent, i++das nicht atomar ist, weil es tatsächlich eine Abkürzung für drei Operationen ist: Abrufen, Hinzufügen und Zuweisen. compareAndSetist sehr nützlich, um Semaphoren, Schlösser, Latches usw. zu implementieren.

Die Verwendung von AtomicIntegerist schneller und besser lesbar als die Verwendung derselben mithilfe der Synchronisierung.

Ein einfacher Test:

public synchronized int incrementNotAtomic() {
    return notAtomic++;
}

public void performTestNotAtomic() {
    final long start = System.currentTimeMillis();
    for (int i = 0 ; i < NUM ; i++) {
        incrementNotAtomic();
    }
    System.out.println("Not atomic: "+(System.currentTimeMillis() - start));
}

public void performTestAtomic() {
    final long start = System.currentTimeMillis();
    for (int i = 0 ; i < NUM ; i++) {
        atomic.getAndIncrement();
    }
    System.out.println("Atomic: "+(System.currentTimeMillis() - start));
}

Auf meinem PC mit Java 1.6 läuft der Atomtest in 3 Sekunden, während der synchronisierte in etwa 5,5 Sekunden läuft. Das Problem hierbei ist, dass die Operation zum Synchronisieren ( notAtomic++) sehr kurz ist. Die Kosten für die Synchronisation sind also im Vergleich zum Betrieb sehr wichtig.

Neben der Atomizität kann AtomicInteger als veränderbare Version von Integerbeispielsweise in Maps als Werte verwendet werden.


1
Ich glaube nicht, dass ich es AtomicIntegerals Kartenschlüssel verwenden möchte , da es die Standardimplementierung verwendet equals(), die mit ziemlicher Sicherheit nicht der Semantik entspricht, die in einer Karte verwendet wird.
Andrzej Doyle

1
@Andrzej sicher, nicht als Schlüssel, der unveränderlich sein muss, sondern als Wert.
Gabuzo

@gabuzo Irgendeine Idee, warum atomare Ganzzahlen über synchronisiert gut abschneiden?
Supun Wijerathne

Der Test ist jetzt ziemlich alt (mehr als 6 Jahre). Es könnte mich interessant machen, ihn mit einem kürzlich durchgeführten JRE erneut zu testen. Ich bin nicht tief genug in die AtomicInteger gegangen, um zu antworten, aber da dies eine sehr spezifische Aufgabe ist, werden Synchronisationstechniken verwendet, die nur in diesem speziellen Fall funktionieren. Denken Sie auch daran, dass der Test monothreaded ist und ein ähnlicher Test in einer stark belasteten Umgebung möglicherweise keinen so klaren Sieg für AtomicInteger
gabuzo

Ich glaube, es ist 3 ms und 5,5 ms
Sathesh

17

Zum Beispiel habe ich eine Bibliothek, die Instanzen einer Klasse generiert. Jede dieser Instanzen muss eine eindeutige Ganzzahl-ID haben, da diese Instanzen Befehle darstellen, die an einen Server gesendet werden, und jeder Befehl muss eine eindeutige ID haben. Da mehrere Threads gleichzeitig Befehle senden dürfen, verwende ich eine AtomicInteger, um diese IDs zu generieren. Ein alternativer Ansatz wäre die Verwendung einer Art Sperre und einer regulären Ganzzahl, aber das ist sowohl langsamer als auch weniger elegant.


Vielen Dank, dass Sie dieses praktische Beispiel geteilt haben. Das klingt nach etwas, das ich verwenden sollte, da ich für jede Datei, die ich in mein Programm importiere, eine eindeutige ID haben muss :)
James P.

7

Wie Gabuzo sagte, verwende ich manchmal AtomicIntegers, wenn ich ein Int als Referenz übergeben möchte. Es ist eine integrierte Klasse mit architekturspezifischem Code, daher ist sie einfacher und wahrscheinlich optimierter als jede MutableInteger, die ich schnell codieren könnte. Das heißt, es fühlt sich wie ein Missbrauch der Klasse an.


7

In Java 8 wurden Atomklassen um zwei interessante Funktionen erweitert:

  • int getAndUpdate (IntUnaryOperator updateFunction)
  • int updateAndGet (IntUnaryOperator updateFunction)

Beide verwenden die updateFunction, um die Aktualisierung des Atomwerts durchzuführen. Der Unterschied besteht darin, dass der erste den alten Wert und der zweite den neuen Wert zurückgibt. Die updateFunction kann implementiert werden, um komplexere "Vergleichs- und Set" -Operationen als die Standardoperation auszuführen. Zum Beispiel kann überprüft werden, ob der Atomzähler nicht unter Null fällt, normalerweise würde eine Synchronisation erforderlich sein, und hier ist der Code sperrenfrei:

    public class Counter {

      private final AtomicInteger number;

      public Counter(int number) {
        this.number = new AtomicInteger(number);
      }

      /** @return true if still can decrease */
      public boolean dec() {
        // updateAndGet(fn) executed atomically:
        return number.updateAndGet(n -> (n > 0) ? n - 1 : n) > 0;
      }
    }

Der Code stammt aus Java Atomic Example .


5

Normalerweise verwende ich AtomicInteger, wenn ich Objekten IDs geben muss, auf die zugegriffen oder aus mehreren Threads erstellt werden kann, und ich verwende es normalerweise als statisches Attribut für die Klasse, auf die ich im Konstruktor der Objekte zugreife.


4

Sie können nicht blockierende Sperren mit compareAndSwap (CAS) für atomare Ganzzahlen oder Longs implementieren. Das Dokument "Tl2" -Software-Transaktionsspeicher beschreibt dies:

Wir ordnen jedem getätigten Speicherort eine spezielle versionierte Schreibsperre zu. In seiner einfachsten Form ist die versionierte Schreibsperre ein Einzelwort-Spinlock, der eine CAS-Operation verwendet, um die Sperre zu erwerben, und einen Speicher, um sie freizugeben. Da man nur ein einziges Bit benötigt, um anzuzeigen, dass die Sperre aufgehoben ist, verwenden wir den Rest des Sperrworts, um eine Versionsnummer zu speichern.

Was es beschreibt, ist zuerst die atomare Ganzzahl zu lesen. Teilen Sie dies in ein ignoriertes Sperrbit und die Versionsnummer auf. Versuchen Sie, CAS als das mit der aktuellen Versionsnummer gelöschte Sperrbit in das gesperrte Sperrbit und die nächste Versionsnummer zu schreiben. Schleife, bis du erfolgreich bist und du der Thread bist, dem die Sperre gehört. Entsperren Sie, indem Sie die aktuelle Versionsnummer bei deaktiviertem Sperrbit einstellen. In diesem Artikel wird die Verwendung der Versionsnummern in den Sperren beschrieben, um zu koordinieren, dass Threads beim Schreiben einen konsistenten Satz von Lesevorgängen aufweisen.

In diesem Artikel wird beschrieben, dass Prozessoren Hardware-Unterstützung für Vergleichs- und Auslagerungsvorgänge bieten, wodurch dies sehr effizient ist. Es behauptet auch:

Nicht blockierende CAS-basierte Zähler, die atomare Variablen verwenden, weisen bei geringen bis mäßigen Konflikten eine bessere Leistung auf als sperrenbasierte Zähler


3

Der Schlüssel ist, dass sie den gleichzeitigen Zugriff und die Änderung sicher ermöglichen. Sie werden häufig als Zähler in einer Multithread-Umgebung verwendet. Vor ihrer Einführung musste dies eine vom Benutzer geschriebene Klasse sein, die die verschiedenen Methoden in synchronisierten Blöcken zusammenfasste.


Aha. Ist dies in Fällen der Fall, in denen ein Attribut oder eine Instanz als eine Art globale Variable innerhalb einer Anwendung fungiert. Oder gibt es andere Fälle, an die Sie denken können?
James P.

1

Ich habe AtomicInteger verwendet, um das Problem des Dining Philosopher zu lösen.

In meiner Lösung wurden AtomicInteger-Instanzen verwendet, um die Gabeln darzustellen. Pro Philosoph werden zwei benötigt. Jeder Philosoph wird als Ganzzahl 1 bis 5 identifiziert. Wenn eine Gabel von einem Philosophen verwendet wird, enthält die AtomicInteger den Wert des Philosophen 1 bis 5, andernfalls wird die Gabel nicht verwendet, sodass der Wert der AtomicInteger -1 beträgt .

Die AtomicInteger ermöglicht es dann, in einer atomaren Operation zu überprüfen, ob eine Gabel frei ist, Wert == - 1, und sie auf den Besitzer der Gabel zu setzen, falls sie frei ist. Siehe Code unten.

AtomicInteger fork0 = neededForks[0];//neededForks is an array that holds the forks needed per Philosopher
AtomicInteger fork1 = neededForks[1];
while(true){    
    if (Hungry) {
        //if fork is free (==-1) then grab it by denoting who took it
        if (!fork0.compareAndSet(-1, p) || !fork1.compareAndSet(-1, p)) {
          //at least one fork was not succesfully grabbed, release both and try again later
            fork0.compareAndSet(p, -1);
            fork1.compareAndSet(p, -1);
            try {
                synchronized (lock) {//sleep and get notified later when a philosopher puts down one fork                    
                    lock.wait();//try again later, goes back up the loop
                }
            } catch (InterruptedException e) {}

        } else {
            //sucessfully grabbed both forks
            transition(fork_l_free_and_fork_r_free);
        }
    }
}

Da die compareAndSet-Methode nicht blockiert, sollte sie den Durchsatz erhöhen und mehr Arbeit leisten. Wie Sie vielleicht wissen, wird das Problem der Dining Philosophen verwendet, wenn ein kontrollierter Zugriff auf Ressourcen erforderlich ist, dh Gabeln, wie ein Prozess Ressourcen benötigt, um die Arbeit fortzusetzen.


0

Einfaches Beispiel für die Funktion compareAndSet ():

import java.util.concurrent.atomic.AtomicInteger; 

public class GFG { 
    public static void main(String args[]) 
    { 

        // Initially value as 0 
        AtomicInteger val = new AtomicInteger(0); 

        // Prints the updated value 
        System.out.println("Previous value: "
                           + val); 

        // Checks if previous value was 0 
        // and then updates it 
        boolean res = val.compareAndSet(0, 6); 

        // Checks if the value was updated. 
        if (res) 
            System.out.println("The value was"
                               + " updated and it is "
                           + val); 
        else
            System.out.println("The value was "
                               + "not updated"); 
      } 
  } 

Der gedruckte Wert lautet: Vorheriger Wert: 0 Der Wert wurde aktualisiert und ist 6 Ein weiteres einfaches Beispiel:

    import java.util.concurrent.atomic.AtomicInteger; 

public class GFG { 
    public static void main(String args[]) 
    { 

        // Initially value as 0 
        AtomicInteger val 
            = new AtomicInteger(0); 

        // Prints the updated value 
        System.out.println("Previous value: "
                           + val); 

         // Checks if previous value was 0 
        // and then updates it 
        boolean res = val.compareAndSet(10, 6); 

          // Checks if the value was updated. 
          if (res) 
            System.out.println("The value was"
                               + " updated and it is "
                               + val); 
        else
            System.out.println("The value was "
                               + "not updated"); 
    } 
} 

Der Ausdruck lautet: Vorheriger Wert: 0 Der Wert wurde nicht aktualisiert

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.