Anleitung zum synchronisierten Schlüsselwort in Java

1. Übersicht

Dieser kurze Artikel ist eine Einführung in die Verwendung des synchronisierten 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 gemeinsam genutzte Daten zu aktualisieren. Java bietet einen Mechanismus zum Vermeiden von Race-Bedingungen, indem der Thread-Zugriff auf gemeinsam genutzte Daten synchronisiert wird.

Ein mit synchronisiert gekennzeichnetes Logikelement wird zu einem synchronisierten Block, sodass jeweils nur ein Thread ausgeführt werden kann .

2. Warum Synchronisation?

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

public class BaeldungSynchronizedMethods { 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); BaeldungSynchronizedMethods summation = new BaeldungSynchronizedMethods(); IntStream.range(0, 1000) .forEach(count -> service.submit(summation::calculate)); service.awaitTermination(1000, TimeUnit.MILLISECONDS); assertEquals(1000, summation.getSum()); }

Wir verwenden einfach einen ExecutorService mit einem 3-Thread-Pool, um berechne () 1000 Mal auszuführen .

Wenn wir dies seriell ausführen würden, wäre die erwartete Ausgabe 1000, aber unsere Multithread-Ausführung schlägt fast jedes Mal mit einer inkonsistenten tatsächlichen Ausgabe fehl, z.

java.lang.AssertionError: expected: but was: 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 mithilfe des synchronisierten Schlüsselworts threadsicher zu machen.

3. Das synchronisierte Schlüsselwort

Das synchronisierte Schlüsselwort kann auf verschiedenen Ebenen verwendet werden:

  • Instanzmethoden
  • Statische Methoden
  • Codeblöcke

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

3.1. Synchronisierte Instanzmethoden

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

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

Beachten Sie, dass nach dem Synchronisieren der Methode der Testfall mit der 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 werden über die Instanz der Klasse synchronisiert , der die Methode gehört. Dies bedeutet, dass nur ein Thread pro Instanz der Klasse diese Methode ausführen kann.

3.2. Synchronisierte Stati c Methods

Statische Methoden werden genau wie Instanzmethoden synchronisiert :

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

Diese Methoden werden für das der Klasse zugeordnete Class- Objekt synchronisiert. Da pro JVM pro Klasse nur ein Class- Objekt vorhanden ist, kann unabhängig von der Anzahl der Instanzen nur ein Thread innerhalb einer statisch synchronisierten Methode pro Klasse ausgeführt werden.

Testen wir es:

@Test public void givenMultiThread_whenStaticSyncMethod() { ExecutorService service = Executors.newCachedThreadPool(); IntStream.range(0, 1000) .forEach(count -> service.submit(BaeldungSynchronizedMethods::syncStaticCalculate)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, BaeldungSynchronizedMethods.staticSum); }

3.3. Synchronisierte Blöcke innerhalb von Methoden

Manchmal möchten wir nicht die gesamte Methode synchronisieren, sondern nur einige darin enthaltene Anweisungen. Dies kann erreicht werden, indem auf einen Block synchronisiert angewendet wird :

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

Testen wir die Änderung:

@Test public void givenMultiThread_whenBlockSync() { ExecutorService service = Executors.newFixedThreadPool(3); BaeldungSynchronizedBlocks synchronizedBlocks = new BaeldungSynchronizedBlocks(); IntStream.range(0, 1000) .forEach(count -> service.submit(synchronizedBlocks::performSynchronisedTask)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, synchronizedBlocks.getCount()); }

Beachten Sie, dass wir ein Parameter übergeben diese an den synchronisierten Block. Dies ist das Monitorobjekt. Der Code im Block wird auf dem Monitorobjekt synchronisiert. Einfach ausgedrückt, kann nur ein Thread pro Monitorobjekt innerhalb dieses Codeblocks ausgeführt werden.

Wenn die Methode statisch ist , übergeben wir den Klassennamen anstelle der Objektreferenz. 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 der statischen Methode:

@Test public void givenMultiThread_whenStaticSyncBlock() { ExecutorService service = Executors.newCachedThreadPool(); IntStream.range(0, 1000) .forEach(count -> service.submit(BaeldungSynchronizedBlocks::performStaticSyncTask)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, BaeldungSynchronizedBlocks.getStaticCount()); }

3.4. Wiedereintritt

Die Sperre hinter den synchronisierten Methoden und Blöcken ist wiedereintrittsfähig. Das heißt, der aktuelle Thread kann immer wieder dieselbe synchronisierte Sperre erhalten, während er sie gedrückt hält:

Object lock = new Object(); synchronized (lock) { System.out.println("First time acquiring it"); synchronized (lock) { System.out.println("Entering again"); synchronized (lock) { System.out.println("And again"); } } }

Wie oben gezeigt, können wir, während wir uns in einem synchronisierten Block befinden, wiederholt dieselbe Monitorsperre erwerben.

4. Fazit

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

Wir haben auch untersucht, wie sich eine Rennbedingung auf unsere Anwendung auswirken kann und wie die Synchronisierung uns dabei hilft, dies zu vermeiden. Weitere Informationen zur Thread-Sicherheit mithilfe von Sperren in Java finden Sie in unserem Artikel java.util.concurrent.Locks .

Der vollständige Code für dieses Tutorial ist auf GitHub verfügbar.