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:

  1. Jeder synchronized Methodenaufruf beginnt mit dem aktuellen Thread, der die Sperre für die Counters Instanz erhält.
  2. Der Thread hält die Sperre, während er den number überprüft und den Zähler aktualisiert.
  3. 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 Zeitpunkt T .

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 .



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow