Heim  >  Artikel  >  Java  >  Wie verwende ich die Atomizitätsfunktion in Java?

Wie verwende ich die Atomizitätsfunktion in Java?

王林
王林nach vorne
2023-05-09 16:40:17925Durchsuche

Thread-Sicherheit

Wenn mehrere Threads auf eine Klasse zugreifen, unabhängig von der von der Laufzeitumgebung verwendeten Planungsmethode oder davon, wie diese Prozesse abwechselnd ausgeführt werden, und ohne zusätzliche Synchronisierung oder Koordination im aufrufenden Code, wird diese Klasse korrekt angezeigt Verhalten wird die Klasse als Thread-sicher bezeichnet.

Thread-Sicherheit spiegelt sich hauptsächlich in den folgenden drei Aspekten wider

  • Atomizität: Bietet sich gegenseitig ausschließenden Zugriff, und nur ein Thread kann gleichzeitig darauf arbeiten

  • Sichtbarkeit: Ein Thread ist dafür verantwortlich Die wichtigsten Speicheränderungen können von anderen Threads zeitnah beobachtet werden.

  • Ordnung: Ein Thread beobachtet die Reihenfolge der Befehlsausführung in anderen Threads. Aufgrund der Neuordnung von Befehlen sind die Beobachtungsergebnisse im Allgemeinen chaotisch und ungeordnet

Detaillierte Erklärung des Atomic-Pakets in JUC

Das Atomic-Paket bietet viele Atomicxxx-Klassen:

Wie verwende ich die Atomizitätsfunktion in Java?

Sie sind alle CAS (compareAndSwap), um Atomizität zu erreichen.

Schreiben Sie zunächst ein einfaches Beispiel wie folgt:

@Slf4j
public class AtomicExample1 { 
    // 请求总数
    public static int clientTotal = 5000; 
    // 同时并发执行的线程数
    public static int threadTotal = 200; 
    public static AtomicInteger count = new AtomicInteger(0); 
    public static void main(String[] args) throws Exception {

        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }
 
    private static void add() {
        count.incrementAndGet();
    }
}

Sie können senden, dass das Ergebnis jeder Operation immer das erwartete Ergebnis 5000 ist, das wir wollen. Erklären Sie, dass diese Zählmethode threadsicher ist.

Werfen wir einen Blick auf die count.incrementAndGet()-Methode. Ihr erster Parameter ist das Objekt selbst, und der zweite Parameter ist valueOffset, der zum Aufzeichnen der kompilierten Adresse des Werts selbst im Speicher dient Der Aktualisierungsvorgang findet den Speicherort des Werts, um den Vergleich zu erleichtern. Der dritte Parameter ist die Konstante 1

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
 
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
 
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
 
    private volatile int value; 
 
    ... 此处省略多个方法...
 
    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
}

Der AtomicInteger-Quellcode verwendet eine Unsafe-Klasse, die eine getAndAddInt-Methode bereitstellt :

public final class Unsafe {
    private static final Unsafe theUnsafe;
 
    ....此处省略很多方法及成员变量.... 
 
 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); 
        return var5;
    } 
 
 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); 
 public native int getIntVolatile(Object var1, long var2);
}

Sie können sehen, dass hier eine do while-Anweisung für die Hauptimplementierung verwendet wird. Der Kern der while-Anweisung besteht darin, eine CompareAndSwapInt()-Methode aufzurufen. Es handelt sich um eine native Methode, bei der es sich um eine Low-Level-Methode handelt, die in Java nicht implementiert ist.

Angenommen, wir möchten eine Operation von 0+1=0 ausführen. Das Folgende sind die Werte jedes Parameters in einem einzelnen Thread:

Wie verwende ich die Atomizitätsfunktion in Java?

Wie verwende ich die Atomizitätsfunktion in Java?

Aktualisiert:

Wie verwende ich die Atomizitätsfunktion in Java?

Methode „compareAndSwapInt()“ Der erste Parameter (var1) ist das aktuelle Objekt, das im Codebeispiel „count“ ist. Zu diesem Zeitpunkt ist sein Wert 0 (erwarteter Wert). Der zweite Wert (var2) ist der übergebene valueOffset-Wert, der den Wert 12 hat. Der dritte Parameter (var4) ist die Konstante 1. Der variable Parameter (var5) in der Methode ist der Wert, der durch Aufrufen der zugrunde liegenden getIntVolatile-Methode basierend auf Parameter eins und Parameter zwei valueOffset erhalten wird. Zu diesem Zeitpunkt ist sein Wert 0. Das Ziel, das vergleichenAndSwapInt() erreichen möchte, besteht darin, dass für das Zählobjekt, wenn der Wert im aktuell erwarteten Wert var1 mit dem zugrunde liegenden zurückgegebenen Wert (var5) übereinstimmt, dieser auf den Wert var5+var4 aktualisiert wird. Wenn unterschiedlich, wiederholen Sie den Vorgang, um den erwarteten Wert (var5) zu erhalten, bis der aktuelle Wert mit dem erwarteten Wert übereinstimmt, und aktualisieren Sie ihn dann. Der Kern der Methode „compareAndSwap“ ist das, was wir normalerweise CAS nennen.

Die Implementierungsprinzipien anderer Klassen im Atomic-Paket, wie z. B. AtomicLong, sind grundsätzlich dieselben wie oben.

Hier stellen wir die LongAdder-Klasse vor. Durch die obige Analyse wissen wir bereits, dass AtomicLong CAS verwendet: Es versucht kontinuierlich, den Zielwert in einer Endlosschleife zu ändern, bis die Änderung erfolgreich ist. Wenn die Konkurrenz nicht groß ist, ist die Wahrscheinlichkeit einer erfolgreichen Änderung sehr hoch. Wenn andererseits in einer Situation mit starkem Wettbewerb die Wahrscheinlichkeit eines Änderungsfehlers hoch ist, werden mehrere Schleifenversuche durchgeführt, sodass die Leistung beeinträchtigt wird.

Für gewöhnliche Long- und Double-Variablen ermöglicht JVM die Aufteilung einer 64-Bit-Leseoperation oder Schreiboperation in zwei 32-Bit-Operationen. Die Kernidee von LongAdder besteht darin, Hotspot-Daten zu trennen. Es kann den internen Kerndatenwert von AtomicLong in ein Array aufteilen und ihn einer der Zahlen zuordnen, um ihn durch Hash und andere Algorithmen zu zählen, wenn jeder Thread darauf zugreift. Das endgültige Zählergebnis ist die Summierung und Akkumulation dieses Arrays. Dabei wird der Hotspot-Datenwert in mehrere Zellen aufgeteilt. Der tatsächliche Wert des aktuellen Objekts wird von allen Zellen akkumuliert. . Auf diese Weise werden Hotspots effektiv getrennt und der Grad der Parallelität verbessert. LongAdder entspricht der Verteilung des Einzelpunkt-Aktualisierungsdrucks auf jeden Knoten auf der Basis von AtomicLong. In Zeiten geringer Parallelität kann eine direkte Aktualisierung der Basis sicherstellen, dass die Leistung im Wesentlichen mit der von Atomic übereinstimmt. In Zeiten hoher Parallelität wird die Leistung durch Dezentralisierung verbessert. Wenn jedoch während der Statistik gleichzeitige Aktualisierungen erfolgen, kann es zu Fehlern in den Statistikdaten kommen.

Wenn die tatsächliche Parallelitätszahl hoch ist, kann LongAdder zuerst verwendet werden. AtomicLong kann zuerst verwendet werden, wenn die Parallelität gering ist oder genaue Werte erforderlich sind, was effizienter ist.

Das Folgende ist eine einfache Demonstration der einfachen Verwendung von AtomicReference unter dem Atomic-Paket:

@Slf4j
public class AtomicExample4 { 
    private static AtomicReference<Integer> count = new AtomicReference<>(0); 
    public static void main(String[] args) {
        count.compareAndSet(0, 2); 
        count.compareAndSet(0, 1); 
        log.info("count:{}", count.get());
    }
}

compareAndSet()分别传入的是预期值跟更新值,只有当预期值跟当前值相等时,才会将值更新为更新值;

上面的第一个方法可以将值更新为2,而第二个步中无法将值更新为1。

下面简单介绍下AtomicIntegerFieldUpdater 用法(利用原子性去更新某个类的实例):

@Slf4j
public class AtomicExample5 { 
    private static AtomicIntegerFieldUpdater<AtomicExample5> updater =
            AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count");
 
    @Getter
    private volatile int count = 100; 
    public static void main(String[] args) { 
        AtomicExample5 example5 = new AtomicExample5();
 
        if (updater.compareAndSet(example5, 100, 120)) {
            log.info("update success 1, {}", example5.getCount());
        }
 
        if (updater.compareAndSet(example5, 100, 120)) {
            log.info("update success 2, {}", example5.getCount());
        } else {
            log.info("update failed, {}", example5.getCount());
        }
    }
}

它可以更新某个类中指定成员变量的值。

注意:修改的成员变量需要用volatile关键字来修饰,并且不能是static描述的字段。

AtomicStampReference这个类它的核心是要解决CAS的ABA问题(CAS操作的时候,其他线程将变量的值A改成了B,接着又改回了A,等线程使用期望值A与当前变量进行比较的时候,发现A变量没有变,于是CAS就将A值进行了交换操作。

实际上该值已经被其他线程改变过)。

ABA问题的解决思路就是每次变量变更的时候,就将版本号加一。

看一下它的一个核心方法compareAndSet():

public class AtomicStampedReference<V> { 
    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
 
   ... 此处省略多个方法 ....
 
   public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
}

可以看到它多了一个stamp的比较,stamp的值是由每次更新的时候进行维护的。

再介绍下AtomicLongArray,它维护了一个数组。在该数组下,我们可以选择性的已原子性操作更新某个索引对应的值。

public class AtomicLongArray implements java.io.Serializable {
    private static final long serialVersionUID = -2308431214976778248L;
 
    private static final Unsafe unsafe = Unsafe.getUnsafe();
 
    ...此处省略....
 
 
    /**
     * Atomically sets the element at position {@code i} to the given value
     * and returns the old value.
     *
     * @param i the index
     * @param newValue the new value
     * @return the previous value
     */
    public final long getAndSet(int i, long newValue) {
        return unsafe.getAndSetLong(array, checkedByteOffset(i), newValue);
    }
 
    /**
     * Atomically sets the element at position {@code i} to the given
     * updated value if the current value {@code ==} the expected value.
     *
     * @param i the index
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int i, long expect, long update) {
        return compareAndSetRaw(checkedByteOffset(i), expect, update);
    }
}

最后再写一个AtomcBoolean的简单使用:

@Slf4j
public class AtomicExample6 { 
    private static AtomicBoolean isHappened = new AtomicBoolean(false);
 
    // 请求总数
    public static int clientTotal = 5000;
 
    // 同时并发执行的线程数
    public static int threadTotal = 200;
 
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    test();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("isHappened:{}", isHappened.get());
    }
 
    private static void test() {
        if (isHappened.compareAndSet(false, true)) {
            log.info("execute");
        }
    }
}

Das obige ist der detaillierte Inhalt vonWie verwende ich die Atomizitätsfunktion in Java?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:yisu.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen