Java Language
Atomtypen
Suche…
Einführung
Java Atomic Types sind einfache veränderliche Typen, die grundlegende Vorgänge ermöglichen, die Thread-sicher und atomar sind, ohne auf Sperren zurückzugreifen. Sie sind für den Einsatz in Fällen vorgesehen, in denen das Sperren ein Engpass bei gleichzeitiger Verwendung wäre oder die Gefahr eines Deadlocks oder Livelock besteht.
Parameter
Parameter | Beschreibung |
---|---|
einstellen | Flüchtiger Satz des Feldes |
erhalten | Flüchtiges Lesen des Feldes |
LazySet | Dies ist eine vom Geschäft geordnete Operation des Feldes |
compareAndSet | Wenn der Wert der Expeed-Wert ist, wird er an den neuen Wert gesendet |
getAndSet | Holen Sie sich den aktuellen Wert und aktualisieren Sie |
Bemerkungen
Viele im Wesentlichen Kombinationen flüchtiger Lese- oder Schreibvorgänge und CAS- Operationen. Der beste Weg, dies zu verstehen, ist der direkte Blick auf den Quellcode. ZB AtomicInteger , Unsafe.getAndSet
Atomtypen erstellen
Bei einfachem Multithreadcode ist die Verwendung der Synchronisierung akzeptabel. Die Verwendung der Synchronisierung hat jedoch Auswirkungen auf die Lebendigkeit. Wenn eine Codebase komplexer wird, steigt die Wahrscheinlichkeit, dass Sie mit Deadlock , Starvation oder Livelock enden.
In Fällen komplexer Parallelität ist die Verwendung von Atomic Variables oft die bessere Alternative, da auf eine einzelne Variable Thread-sicher zugegriffen werden kann, ohne synchronisierte Methoden oder Codeblöcke verwenden zu müssen.
Einen AtomicInteger
Typ AtomicInteger
:
AtomicInteger aInt = new AtomicInteger() // Create with default value 0
AtomicInteger aInt = new AtomicInteger(1) // Create with initial value 1
Ähnlich für andere Instanztypen.
AtomicIntegerArray aIntArray = new AtomicIntegerArray(10) // Create array of specific length
AtomicIntegerArray aIntArray = new AtomicIntegerArray(new int[] {1, 2, 3}) // Initialize array with another array
Ähnlich für andere Atomtypen.
Es gibt eine bemerkenswerte Ausnahme, dass es keine float
und double
gibt. Diese können durch Verwendung von Float.floatToIntBits(float)
und Float.intBitsToFloat(int)
für float
sowie Double.doubleToLongBits(double)
und Double.longBitsToDouble(long)
für Doppel Double.longBitsToDouble(long)
.
Wenn Sie sun.misc.Unsafe
verwenden sun.misc.Unsafe
, können Sie jede primitive Variable als atomar verwenden, indem Sie die atomare Operation in sun.misc.Unsafe
. Alle primitiven Typen sollten in int oder long konvertiert oder codiert werden, um sie auf diese Weise zu verwenden. Weitere Informationen hierzu finden Sie unter: sun.misc.Unsafe .
Motivation für Atomtypen
Der einfachste Weg, Multithread-Anwendungen zu implementieren, ist die Verwendung der integrierten Synchronisations- und Sperr-Grundelemente von Java. zB das synchronized
Schlüsselwort. Das folgende Beispiel zeigt, wie wir synchronized
, um Zählwerte zu sammeln.
public class Counters {
private final int[] counters;
public Counters(int nosCounters) {
counters = new int[nosCounters];
}
/**
* Increments the integer at the given index
*/
public synchronized void count(int number) {
if (number >= 0 && number < counters.length) {
counters[number]++;
}
}
/**
* Obtains the current count of the number at the given index,
* or if there is no number at that index, returns 0.
*/
public synchronized int getCount(int number) {
return (number >= 0 && number < counters.length) ? counters[number] : 0;
}
}
Diese Implementierung wird korrekt funktionieren. Wenn jedoch eine große Anzahl von Threads viele gleichzeitige Aufrufe für dasselbe Counters
Objekt durchführt, kann die Synchronisierung einen Engpass darstellen. Speziell:
- Jeder
synchronized
Methodenaufruf beginnt mit dem aktuellen Thread, der die Sperre für dieCounters
Instanz erhält. - Der Thread hält die Sperre, während er den
number
überprüft und den Zähler aktualisiert. - Schließlich gibt er die Sperre frei und ermöglicht anderen Threads den Zugriff.
Wenn ein Thread versucht, die Sperre abzurufen, während ein anderer die Sperre aufrechterhält, wird der versuchte Thread in Schritt 1 blockiert (angehalten), bis die Sperre freigegeben wird. Wenn mehrere Threads warten, wird einer von ihnen darauf zugreifen und die anderen werden weiterhin blockiert.
Dies kann zu einigen Problemen führen:
Wenn es viele Konflikte um die Sperre gibt (dh viele Threads versuchen, sie zu erhalten), können einige Threads für lange Zeit blockiert werden.
Wenn ein Thread blockiert ist und auf die Sperre wartet, versucht das Betriebssystem normalerweise, die Ausführung auf einen anderen Thread umzustellen. Diese Kontextumschaltung verursacht eine relativ große Auswirkung auf die Leistung auf den Prozessor.
Wenn mehrere Threads für dieselbe Sperre blockiert sind, kann nicht garantiert werden, dass einer von ihnen "fair" behandelt wird (dh, dass jeder Thread garantiert zur Ausführung geplant ist). Dies kann zu einem Fadenknappheit führen .
Wie implementiert man Atomtypen?
Beginnen wir mit dem AtomicInteger
des obigen Beispiels mit AtomicInteger
Zählern:
public class Counters {
private final AtomicInteger[] counters;
public Counters(int nosCounters) {
counters = new AtomicInteger[nosCounters];
for (int i = 0; i < nosCounters; i++) {
counters[i] = new AtomicInteger();
}
}
/**
* Increments the integer at the given index
*/
public void count(int number) {
if (number >= 0 && number < counters.length) {
counters[number].incrementAndGet();
}
}
/**
* Obtains the current count of the object at the given index,
* or if there is no number at that index, returns 0.
*/
public int getCount(int number) {
return (number >= 0 && number < counters.length) ?
counters[number].get() : 0;
}
}
Wir haben das int[]
durch ein AtomicInteger[]
und es mit einer Instanz in jedem Element initialisiert. Wir haben auch Aufrufe von incrementAndGet()
und get()
anstelle von Operationen für int
Werte hinzugefügt.
Das Wichtigste ist jedoch, dass wir das synchronized
Schlüsselwort entfernen können, da das Sperren nicht mehr erforderlich ist. Dies funktioniert, weil die Operationen incrementAndGet()
und get()
atomar und threadsicher sind . In diesem Zusammenhang bedeutet dies:
Jeder Zähler in dem Feld wird nur in entweder den „vor dem “ Zustand für eine Operation (wie ein „Inkrement“) oder in dem „nach“ Zustand beobachtbar sein.
Unter der Annahme, dass die Operation zum Zeitpunkt
T
auftritt, kann kein Thread den Zustand "vor" nach dem ZeitpunktT
.
Während zwei Threads tatsächlich versuchen können, dieselbe AtomicInteger
Instanz gleichzeitig zu aktualisieren, stellen die Implementierungen der Vorgänge außerdem sicher, dass jeweils nur ein Inkrement für die angegebene Instanz erfolgt. Dies erfolgt ohne Verriegelung, was häufig zu einer besseren Leistung führt.
Wie funktionieren Atomtypen?
Atomtypen basieren normalerweise auf speziellen Hardwareanweisungen im Befehlssatz des Zielcomputers. Beispielsweise bieten Intel-basierte Befehlssätze einen CAS
-Befehl ( Compare and Swap ), der eine bestimmte Folge von Speicheroperationen atomar ausführt.
Diese Anweisungen auf niedriger Ebene werden verwendet, um Vorgänge höherer Ebene in den APIs der jeweiligen AtomicXxx
Klassen zu AtomicXxx
. Zum Beispiel (wieder in C-artigem Pseudocode):
private volatile num;
int increment() {
while (TRUE) {
int old = num;
int new = old + 1;
if (old == compare_and_swap(&num, old, new)) {
return new;
}
}
}
Wenn auf dem AtomicXxxx
kein Konflikt AtomicXxxx
, ist der if
Test erfolgreich, und die Schleife endet sofort. Wenn es Konflikte gibt, wird das if
für alle außer einem der Threads fehlschlagen, und sie werden sich in der Schleife für eine kleine Anzahl von Zyklen der Schleife "drehen". In der Praxis ist das Drehen um Größenordnungen schneller (außer bei unrealistisch hohen Konflikten, bei denen synchronisierte besser als Atomklassen abschneidet, da bei einem CAS-Vorgang der Versuch nur mehr Konflikte hinzufügt), als den Thread anzuhalten und zu einem anderen zu wechseln ein.
Im Übrigen werden CAS-Anweisungen normalerweise von der JVM verwendet, um ein unkontrolliertes Sperren zu implementieren. Wenn die JVM erkennt, dass eine Sperre derzeit nicht gesperrt ist, versucht sie, ein CAS zum Abrufen der Sperre zu verwenden. Wenn der CAS erfolgreich ist, müssen keine teuren Thread-Zeitpläne, Kontextwechsel usw. durchgeführt werden. Weitere Informationen zu den verwendeten Techniken finden Sie unter Verzerrte Sperren in HotSpot .