Einführung in die Synchronisation in Java

Die Synchronisierung ist eine Java-Funktion, die verhindert, dass mehrere Threads gleichzeitig auf die gemeinsam genutzten Ressourcen zugreifen. Hier beziehen sich gemeinsam genutzte Ressourcen auf externe Dateiinhalte, Klassenvariablen oder Datenbankeinträge.

Die Synchronisation ist in der Multithread-Programmierung weit verbreitet. "Synchronisiert" ist das Schlüsselwort, mit dem Ihr Code die Möglichkeit erhält, dass nur ein einzelner Thread in diesem Zeitraum ohne Beeinträchtigung durch einen anderen Thread darauf zugreifen kann.

Warum brauchen wir die Synchronisation in Java?

  • Java ist eine Multithread-Programmiersprache. Dies bedeutet, dass zwei oder mehr Threads gleichzeitig ausgeführt werden können, um eine Aufgabe abzuschließen. Wenn Threads gleichzeitig ausgeführt werden, ist die Wahrscheinlichkeit groß, dass ein Szenario auftritt, in dem Ihr Code unerwartete Ergebnisse liefert.
  • Sie fragen sich vielleicht, warum Multithreading in Java eine wichtige Funktion darstellt, wenn es zu fehlerhaften Ausgaben führen kann.
  • Multithreading beschleunigt Ihren Code, indem mehrere Threads gleichzeitig ausgeführt werden. Dadurch wird die Ausführungszeit Ihres Codes verkürzt und eine hohe Leistung erzielt. Die Verwendung der Multithreading-Umgebung führt jedoch aufgrund einer allgemein als Race-Bedingung bekannten Bedingung zu ungenauen Ausgaben.

Was ist eine Rennbedingung?

Wenn zwei oder mehr Threads gleichzeitig ausgeführt werden, können sie zu diesem Zeitpunkt auf freigegebene Ressourcen zugreifen und diese ändern. Die Sequenzen, in denen die Threads ausgeführt werden, werden vom Thread-Scheduling-Algorithmus festgelegt.

Aufgrund dessen kann man nicht vorhersagen, in welcher Reihenfolge Threads ausgeführt werden, da dies ausschließlich vom Thread-Scheduler gesteuert wird. Dies wirkt sich auf die Ausgabe des Codes aus und führt zu inkonsistenten Ausgaben. Da mehrere Threads miteinander im Rennen sind, um den Vorgang abzuschließen, wird die Bedingung als "Rennbedingung" bezeichnet.

Betrachten wir zum Beispiel den folgenden Code:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "+ Thread.currentThread().getName() + "Current Thread value " + this.getMyVar());
)
)
Class RaceCondition:
package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj, "thread 2");
Thread t3 = new Thread(mObj, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Wenn Sie den obigen Code nacheinander ausführen, sind die Ausgaben wie folgt:

Ourput1:

Aktueller Thread wird ausgeführt Thread 1 Aktueller Thread-Wert 3

Aktueller Thread wird ausgeführt Thread 3 Aktueller Thread-Wert 2

Aktueller Thread wird ausgeführt Thread 2 Aktueller Thread-Wert 3

Output2:

Aktueller Thread wird ausgeführt Thread 3 Aktueller Thread-Wert 3

Aktueller Thread wird ausgeführt Thread 2 Aktueller Thread-Wert 3

Aktueller Thread wird ausgeführt Thread 1 Aktueller Thread-Wert 3

Output3:

Aktueller Thread wird ausgeführt Thread 2 Aktueller Thread-Wert 3

Aktueller Thread wird ausgeführt Thread 1 Aktueller Thread-Wert 3

Aktueller Thread wird ausgeführt Thread 3 Aktueller Thread-Wert 3

Output4:

Aktueller Thread wird ausgeführt Thread 1 Aktueller Thread-Wert 2

Aktueller Thread wird ausgeführt Thread 3 Aktueller Thread-Wert 3

Aktueller Thread wird ausgeführt Thread 2 Aktueller Thread-Wert 2

  • Aus dem obigen Beispiel können Sie schließen, dass die Threads zufällig ausgeführt werden und der Wert auch falsch ist. Nach unserer Logik sollte der Wert um 1 erhöht werden. Hier beträgt der Ausgabewert jedoch in den meisten Fällen 3 und in einigen Fällen 2.
  • Hier ist die Variable "myVar" die gemeinsam genutzte Ressource, auf der mehrere Threads ausgeführt werden. Die Threads greifen gleichzeitig auf den Wert von "myVar" zu und ändern ihn. Lassen Sie uns sehen, was passiert, wenn wir die anderen beiden Threads auskommentieren.

Die Ausgabe in diesem Fall ist:

Der aktuelle Thread wird ausgeführt. Thread 1 Aktueller Thread-Wert 1

Das heißt, wenn ein einzelner Thread ausgeführt wird, ist die Ausgabe wie erwartet. Wenn jedoch mehrere Threads ausgeführt werden, wird der Wert von jedem Thread geändert. Daher muss die Anzahl der Threads, die an einer freigegebenen Ressource arbeiten, auf jeweils einen Thread beschränkt werden. Dies wird durch Synchronisation erreicht.

Grundlegendes zur Synchronisation in Java

  • Die Synchronisation in Java erfolgt mit Hilfe des Schlüsselworts „synchronized“. Dieses Schlüsselwort kann für Methoden, Blöcke oder Objekte verwendet werden, jedoch nicht für Klassen und Variablen. Ein synchronisierter Code ermöglicht es nur einem Thread, auf ihn zu einem bestimmten Zeitpunkt zuzugreifen und ihn zu ändern.
  • Ein synchronisierter Code beeinträchtigt jedoch die Codeleistung, da andere Threads, die versuchen, auf ihn zuzugreifen, länger warten müssen. Ein Teil des Codes sollte daher nur synchronisiert werden, wenn die Möglichkeit besteht, dass eine Racebedingung eintritt. Wenn nicht, sollte man es vermeiden.

Wie funktioniert die Synchronisation in Java intern?

  • Die interne Synchronisation in Java wurde mit Hilfe des Lock-Konzepts (auch als Monitor bezeichnet) implementiert. Jedes Java-Objekt hat eine eigene Sperre. In einem synchronisierten Codeblock muss ein Thread die Sperre erwerben, bevor er diesen bestimmten Codeblock ausführen kann. Sobald ein Thread die Sperre erhält, kann er diesen Code ausführen.
  • Nach Abschluss der Ausführung wird die Sperre automatisch aufgehoben. Wenn ein anderer Thread den synchronisierten Code verarbeiten muss, wartet er darauf, dass der aktuelle Thread, der darauf ausgeführt wird, die Sperre aufhebt. Dieser Prozess des Erfassens und Freigebens von Sperren wird intern von der Java Virtual Machine ausgeführt. Ein Programm ist nicht dafür verantwortlich, Sperren durch den Thread zu erlangen und freizugeben. Die verbleibenden Threads können jedoch jeden anderen nicht synchronisierten Code gleichzeitig ausführen.

Synchronisieren wir unser vorheriges Beispiel, indem wir den Code innerhalb der run-Methode mit dem synchronisierten Block in der Klasse "Modify" wie folgt synchronisieren:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)
)

Der Code für die Klasse „RaceCondition“ bleibt gleich. Beim Ausführen des Codes sieht die Ausgabe folgendermaßen aus:

Output1:

Der aktuelle Thread wird ausgeführt. Thread 1 Aktueller Thread-Wert 1

Der aktuell ausgeführte Thread Thread 2 Aktueller Thread-Wert 2

Der aktuelle Thread wird ausgeführt. Thread 3 Aktueller Thread-Wert 3

Output2:

Der aktuelle Thread wird ausgeführt. Thread 1 Aktueller Thread-Wert 1

Der aktuell ausgeführte Thread Thread 3 Aktueller Thread-Wert 2

Der aktuell ausgeführte Thread Thread 2 Aktueller Thread-Wert 3

Beachten Sie, dass unser Code die erwartete Ausgabe liefert. Hier erhöht jeder Thread den Wert für die Variable „myVar“ (in der Klasse „Modify“) um 1.

Hinweis: Die Synchronisierung ist erforderlich, wenn mehrere Threads auf demselben Objekt ausgeführt werden. Wenn mehrere Threads auf mehreren Objekten ausgeführt werden, ist keine Synchronisierung erforderlich.

Ändern Sie beispielsweise den Code in der Klasse "RaceCondition" wie folgt und arbeiten Sie mit der zuvor nicht synchronisierten Klasse "Modify".

package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Modify mObj1 = new Modify();
Modify mObj2 = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj1, "thread 2");
Thread t3 = new Thread(mObj2, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Ausgabe:

Der aktuelle Thread wird ausgeführt. Thread 1 Aktueller Thread-Wert 1

Der aktuell ausgeführte Thread Thread 2 Aktueller Thread-Wert 1

Der aktuelle Thread wird ausgeführt. Thread 3 Aktueller Thread-Wert 1

Arten der Synchronisation in Java:

Es gibt zwei Arten der Thread-Synchronisation, von denen sich eine ausschließt und die andere die Kommunikation zwischen Threads.

1. gegenseitig exklusiv

  • Synchronisierte Methode.
  • Statische synchronisierte Methode
  • Synchronisierter Block.

2. Thread-Koordination (Inter-Thread-Kommunikation in Java)

Sich gegenseitig ausschließen:

  • In diesem Fall erhalten Threads die Sperre, bevor sie ein Objekt bearbeiten, wodurch vermieden wird, mit Objekten zu arbeiten, deren Werte von anderen Threads bearbeitet wurden.
  • Dies kann auf drei Arten erreicht werden:

ich. Synchronisierte Methode: Wir können das Schlüsselwort "synchronized" für eine Methode verwenden und sie somit zu einer synchronisierten Methode machen. Jeder Thread, der die synchronisierte Methode aufruft, erhält die Sperre für dieses Objekt und gibt sie frei, sobald der Vorgang abgeschlossen ist. Im obigen Beispiel können wir unsere Methode "run ()" mithilfe des Schlüsselworts "synchronized" nach dem Zugriffsmodifikator als synchronisieren festlegen.

@Override
public synchronized void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)

Die Ausgabe für diesen Fall lautet:

Der aktuelle Thread wird ausgeführt. Thread 1 Aktueller Thread-Wert 1

Der aktuell ausgeführte Thread Thread 3 Aktueller Thread-Wert 2

Der aktuell ausgeführte Thread Thread 2 Aktueller Thread-Wert 3

ii. Statische synchronisierte Methode: Um statische Methoden zu synchronisieren, muss die Sperre auf Klassenebene aktiviert werden. Nachdem ein Thread die Sperre auf Klassenebene erhalten hat, kann er eine statische Methode ausführen. Während ein Thread die Klassenebenensperre enthält, kann kein anderer Thread eine andere statische synchronisierte Methode dieser Klasse ausführen. Die anderen Threads können jedoch jede andere reguläre Methode oder reguläre statische Methode oder sogar nicht statische synchronisierte Methode dieser Klasse ausführen.

Betrachten wir beispielsweise unsere Klasse "Modify" und nehmen Sie Änderungen daran vor, indem Sie unsere Methode "increment" in eine statische synchronisierte Methode konvertieren. Die Codeänderungen sind wie folgt:

package JavaConcepts;
public class Modify implements Runnable(
private static int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public static synchronized void increment() (
myVar++;
System.out.println("Current thread being executed " + Thread.currentThread().getName() + " Current Thread value " + myVar);
)
@Override
public void run() (
// TODO Auto-generated method stub
increment();
)
)

iii. Synchronisierter Block: Einer der Hauptnachteile der synchronisierten Methode besteht darin, dass die Wartezeit für Threads erhöht wird, was sich auf die Leistung des Codes auswirkt. Um daher anstelle der gesamten Methode nur die erforderlichen Codezeilen synchronisieren zu können, muss ein synchronisierter Block verwendet werden. Die Verwendung des synchronisierten Blocks verringert die Wartezeit der Threads und verbessert auch die Leistung. Im vorherigen Beispiel haben wir bereits den synchronisierten Block verwendet, während wir unseren Code zum ersten Mal synchronisierten.

Beispiel:
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)

Thread-Koordination:

Für synchronisierte Threads ist die Kommunikation zwischen Threads eine wichtige Aufgabe. Eingebaute Methoden, die dabei helfen, eine Inter-Thread-Kommunikation für synchronisierten Code zu erreichen, sind:

  • warten()
  • benachrichtigen()
  • notifyAll ()

Hinweis: Diese Methoden gehören zur Objektklasse und nicht zur Thread-Klasse. Damit ein Thread diese Methoden für ein Objekt aufrufen kann, muss er die Sperre für dieses Objekt halten. Diese Methoden bewirken auch, dass ein Thread seine Sperre für das Objekt aufhebt, für das er aufgerufen wird.

wait (): Ein Thread, der die wait () - Methode aufruft, die Sperre für das Objekt aufhebt und in den Wartezustand wechselt. Es gibt zwei Methodenüberladungen:

  • public final void wait () löst InterruptedException aus
  • public final void wait (lange Wartezeit) löst InterruptedException aus
  • public final void wait (lange Wartezeit, int nanos) löst InterruptedException aus

notify (): Ein Thread sendet unter Verwendung der notify () -Methode ein Signal an einen anderen Thread im Wartezustand. Es sendet die Benachrichtigung nur an einen Thread, sodass dieser Thread seine Ausführung fortsetzen kann. Welcher Thread die Benachrichtigung unter allen Threads im Wartezustand erhält, hängt von der Java Virtual Machine ab.

  • öffentliche abschließende nichtige Benachrichtigung ()

notifyAll (): Wenn ein Thread die Methode notifyAll () aufruft, wird jeder Thread in seinem Wartestatus benachrichtigt. Diese Threads werden nacheinander in der von der Java Virtual Machine festgelegten Reihenfolge ausgeführt.

  • public final void notifyAll ()

Fazit

In diesem Artikel haben wir gesehen, wie das Arbeiten in einer Umgebung mit mehreren Threads aufgrund einer Racebedingung zu Dateninkonsistenz führen kann. Wie die Synchronisierung dabei hilft, dies zu überwinden, indem ein einzelner Thread auf eine gemeinsam genutzte Ressource beschränkt wird. Auch wie synchronisierte Threads miteinander kommunizieren.

Empfohlene Artikel:

Dies war eine Anleitung zu Was ist Synchronisation in Java ?. Hier diskutieren wir die Einführung, das Verständnis, die Notwendigkeit, die Arbeitsweise und die Arten der Synchronisation mit einem Beispielcode. Sie können auch unsere anderen Artikelvorschläge durchgehen, um mehr zu erfahren -

  1. Serialisierung in Java
  2. Was ist Generics in Java?
  3. Was ist API in Java?
  4. Was ist ein binärer Baum in Java?
  5. Beispiele und Funktionsweise von Generika in C #