Anleitung zum synchronisierten Schlüsselwort in Java

Anleitung zum synchronisierten Schlüsselwort in Java

1. Überblick

Dieser kurze Artikel ist eine Einführung in die Verwendung dessynchronized-Blocks in Java.

Einfach ausgedrückt, tritt in einer Umgebung mit mehreren Threads eine Race-Bedingung auf, wenn zwei oder mehr Threads gleichzeitig versuchen, veränderbare gemeinsame Daten zu aktualisieren. Java bietet einen Mechanismus zur Vermeidung von Race-Bedingungen, indem der Thread-Zugriff auf gemeinsam genutzte Daten synchronisiert wird.

Ein mitsynchronized markiertes Stück Logik wird zu einem synchronisierten Blockallowing only one thread to execute at any given time.

2. Warum Synchronisation?

Betrachten wir eine typische Race-Bedingung, bei der wir die Summe berechnen und mehrere Threads diecalculate()-Methode ausführen:

public class exampleSynchronizedMethods {

    private int sum = 0;

    public void calculate() {
        setSum(getSum() + 1);
    }

    // standard setters and getters
}

Und schreiben wir einen einfachen Test:

@Test
public void givenMultiThread_whenNonSyncMethod() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    exampleSynchronizedMethods summation = new exampleSynchronizedMethods();

    IntStream.range(0, 1000)
      .forEach(count -> service.submit(summation::calculate));
    service.awaitTermination(1000, TimeUnit.MILLISECONDS);

    assertEquals(1000, summation.getSum());
}

Wir verwenden einfach einExecutorService mit einem 3-Thread-Pool, um dascalculate() 1000 Mal auszuführen.

Wenn wir dies seriell ausführen würden, wäre die erwartete Ausgabe 1000, aberour multi-threaded execution fails almost every time mit einer inkonsistenten tatsächlichen Ausgabe, z.

java.lang.AssertionError: expected:<1000> but was:(965)
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
...

Dieses Ergebnis ist natürlich nicht unerwartet.

Eine einfache Möglichkeit, die Race-Bedingung zu vermeiden, besteht darin, die Operation threadsicher zu machen, indem Sie das Schlüsselwortsynchronizedverwenden.

3. Das SchlüsselwortSynchronized

Das Schlüsselwortsynchronizedkann auf verschiedenen Ebenen verwendet werden:

  • Instanzmethoden

  • Statische Methoden

  • Codeblöcke

Wenn wir einensynchronized-Block verwenden, verwendet Java intern einen Monitor, der auch als Monitorsperre oder intrinsische Sperre bezeichnet wird, um die Synchronisation bereitzustellen. Diese Monitore sind an ein Objekt gebunden, sodass alle synchronisierten Blöcke desselben Objekts gleichzeitig von nur einem Thread ausgeführt werden können.

3.1. Synchronized Instanzmethoden

Fügen Sie einfach das Schlüsselwortsynchronized in die Methodendeklaration ein, um die Methode zu synchronisieren:

public synchronized void synchronisedCalculate() {
    setSum(getSum() + 1);
}

Beachten Sie, dass der Testfall nach dem Synchronisieren der Methode mit einer tatsächlichen Ausgabe von 1000 bestanden wird:

@Test
public void givenMultiThread_whenMethodSync() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    SynchronizedMethods method = new SynchronizedMethods();

    IntStream.range(0, 1000)
      .forEach(count -> service.submit(method::synchronisedCalculate));
    service.awaitTermination(1000, TimeUnit.MILLISECONDS);

    assertEquals(1000, method.getSum());
}

Instanzmethoden sindsynchronized über der Instanz der Klasse, der die Methode gehört. Dies bedeutet, dass nur ein Thread pro Instanz der Klasse diese Methode ausführen kann.

3.2. Synchronized Static Methoden

Statische Methoden sindsynchronized genau wie Instanzmethoden:

 public static synchronized void syncStaticCalculate() {
     staticSum = staticSum + 1;
 }

Diese Methoden sindsynchronized für das der Klasse zugeordneteClass-Objekt. Da pro JVM und Klasse nur einClass-Objekt vorhanden ist, kann nur ein Thread innerhalb einesstaticsynchronized Methode pro Klasse, unabhängig von der Anzahl der Instanzen.

Testen wir es:

@Test
public void givenMultiThread_whenStaticSyncMethod() {
    ExecutorService service = Executors.newCachedThreadPool();

    IntStream.range(0, 1000)
      .forEach(count ->
        service.submit(exampleSynchronizedMethods::syncStaticCalculate));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, exampleSynchronizedMethods.staticSum);
}

3.3. Synchronized Blöcke innerhalb von Methoden

Manchmal möchten wir nicht die gesamte Methode synchronisieren, sondern nur einige Anweisungen darin. Dies kann durchapplying erreicht werden, die mit einem Block synchronisiert sind:

public void performSynchrinisedTask() {
    synchronized (this) {
        setCount(getCount()+1);
    }
}

Lassen Sie uns die Änderung testen:

@Test
public void givenMultiThread_whenBlockSync() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    exampleSynchronizedBlocks synchronizedBlocks = new exampleSynchronizedBlocks();

    IntStream.range(0, 1000)
      .forEach(count ->
        service.submit(synchronizedBlocks::performSynchronisedTask));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, synchronizedBlocks.getCount());
}

Beachten Sie, dass wir einen Parameterthis an den Blocksynchronized übergeben haben. Dies ist das Monitorobjekt. Der Code innerhalb des Blocks wird auf dem Monitorobjekt synchronisiert. Einfach ausgedrückt, kann in diesem Codeblock nur ein Thread pro Monitorobjekt ausgeführt werden.

Wenn die Methodestatic, ist, würden wir den Klassennamen anstelle der Objektreferenz übergeben. Und die Klasse wäre ein Monitor für die Synchronisation des Blocks:

public static void performStaticSyncTask(){
    synchronized (SynchronisedBlocks.class) {
        setStaticCount(getStaticCount() + 1);
    }
}

Testen wir den Block innerhalb derstatic-Methode:

@Test
public void givenMultiThread_whenStaticSyncBlock() {
    ExecutorService service = Executors.newCachedThreadPool();

    IntStream.range(0, 1000)
      .forEach(count ->
        service.submit(exampleSynchronizedBlocks::performStaticSyncTask));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, exampleSynchronizedBlocks.getStaticCount());
}

5. Fazit

In diesem kurzen Artikel haben wir verschiedene Möglichkeiten gesehen, das Schlüsselwortsynchronized zu verwenden, um eine Thread-Synchronisation zu erreichen.

Wir haben auch untersucht, wie sich eine Race-Bedingung auf unsere Anwendung auswirken kann und wie die Synchronisierung dabei hilft, dies zu vermeiden. Weitere Informationen zur Thread-Sicherheit mithilfe von Sperren in Java finden Sie in unserenjava.util.concurrent.Locksarticle.

Der vollständige Code für dieses Tutorial istover on GitHub verfügbar.