Antworten:
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).
read
und ä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.
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 AtomicIntegers
zum 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 ints
und entsprechende synchronized
Deklarationen erzielen können , besteht das Schöne daran, AtomicInteger
dass 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 int
Wert 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.
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
++i
lautet wie folgt : is i.incrementAndGet()
i++
is i.getAndIncrement()
--i
is i.decrementAndGet()
i--
is i.getAndDecrement()
i = x
is i.set(x)
x = i
is isx = i.get()
Es gibt auch andere Bequemlichkeitsmethoden wie compareAndSet
oderaddAndGet
Die Hauptverwendung von AtomicInteger
ist, 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 int
sind bereits atomar, es gibt jedoch AtomicInteger
viele Operationen, die nicht atomar sind int
.
Am einfachsten sind die getAndXXX
oder 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. compareAndSet
ist sehr nützlich, um Semaphoren, Schlösser, Latches usw. zu implementieren.
Die Verwendung von AtomicInteger
ist 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 Integer
beispielsweise in Map
s als Werte verwendet werden.
AtomicInteger
als 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.
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.
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.
In Java 8 wurden Atomklassen um zwei interessante Funktionen erweitert:
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 .
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.
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
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.
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.
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