Advanced   Java   Services Synchronisieren mit synchronized


Das Schlüsselwort synchronized

Es gibt bekanntlich Resourcen, die exklusiv vergeben werden müssen, hierzu gehören u.a. Ausgaben auf den Drucker und Schreibzugriffe auf Dateien. Damit ein Thread eine Aufgabe exklusiv abarbeiten kann stellt Java das Schlüsselwort synchronized bereit. Betritt ein Thread einen Codebereich, der mit synchronized geschützt ist, kann er diesen Bereich exklusiv abarbeiten. Kein anderer Thread kann dann diesen Bereich betreten. Dazu hat jedes Javaobjekt einen Monitor. Diese Überwachungsinstanz enthält pro Objekt genau einen Lock. Will ein Thread einen mit synchronized geschützten Bereich betreten, so muß er vom zugehörigen Objekt einen Lock anfordern. Ist dieser bereits vergeben, so muß er warten. Da es nur einen Lock gibt sperrt ein Thread, der diesen Lock besitzt zwangsläufig alle anderen mit synchronized geschützten Bereiche dieses Objekts. Es werden damit evtl. auch Bereiche gesperrt, die mit der von dem Thread gerade abgearbeiteten Code garnichts zu tun haben. Performanceverluste sind die logische Folge dieses sozusagen rabiaten Verhaltens.


Der Einsatz von synchronized

Mit synchronized kann man einen bestimmten Codebereich schützen oder auch eine ganze Methode. Im ersten Fall spricht man von einem synchronized-Block.


Der synchronized Block
synchronized(einObjekt)
{
   // statements
}

Dieser Bereich wird nun durch das angegebene Objekt geschützt, das heißt, daß "einObjekt" den Lock vergibt. Oft ist dieses Objekt this, da sich der Block in einer nichtstatischen Methode des Objekts "einObjekt" befindet.


Die synchronized Methode
public synchronized void method()
{
   // statements
}

Hier wird die gesamte Methode exklusiv vergeben. Natürlich ist jeder beliebige Returntyp möglich und auch andere Schutzzustände. Die obige Schreibweise ist lediglich eine Abkürzung für die folgende:

public void method()
{
   synchronized(this)
   {
      // statements
   }
}

Statische synchronized Blöcke und statische synchronized Methoden

Auch auf der Ebene static kann man mit synchronized arbeiten. Da ein synchronized Block ein Objekt benötigt nimmt man in diesem Fall das der Klasse zugeordnete class-Objekt.


Der statische synchronized Block
synchronized(MyKlassenName.class)
{
   // statements
}

Dieser Bereich wird nun durch das angegebene class-Objekt geschützt.


Die statische synchronized Methode
public static synchronized void method()
{
   // statements
}

Wie zu erwarten ist obige Schreibweise äquivalent zu der folgenden.

public static void method()
{
   synchronized(MyKlassenName.class)
   {
      // statements
   }
}

Der Name der Klasse ist natürlich der in dem die Methode angesiedelt ist.


Ein Beispiel

Das folgende Beispiel zeigt eine statische Methode, die sowohl einen geschützten Bereich wie auch einen ungeschützten Bereich enthält. Beide Bereiche geben 10 Zeichen auf die Konsole aus. Dazu gibt es eine Threadklasse, die mit dieser Methode arbeitet. Den Threadobjekten kann übergeben werden welche Strings sie in welchem Bereich ausgeben sollen. Damit die beiden Bereiche nicht zu schnell ablaufen werden sie durch sleep gebremst. Der erste Thread gibt im ungeschützten Bereich "1" und im geschützten Bereich "A" aus, der zweite Thread im ungeschützten Bereich "2" und im geschützten Bereich "B" aus.

Die Hauptklasse mit der statischen Methode print

public class SynchronizedDemo
{
   public static void main(String[] args)
   {
      Thread th1 = new AusgabeThread("1","A");
      Thread th2 = new AusgabeThread("2","B");
      th1.start();
      th2.start();

   }

   public static void print(String s1, String s2)
   {
      for(int i = 0; i < 10; i++)
      {
         System.out.print(s1);
         try
         {
            Thread.sleep(200);
         }
         catch(InterruptedException ex)
         {
         }
      }

      synchronized(SynchronizedDemo.class)
      {
         for(int i = 0; i < 10; i++)
         {
            System.out.print(s2);
            try
            {
               Thread.sleep(200);
            }
            catch(InterruptedException ex)
            {
            }
         }
      }
   }
}

Die Threadklasse

public class AusgabeThread extends Thread
{
   private String s1;
   private String s2;

   public AusgabeThread(String s1, String s2)
   {
      this.s1 = s1;
      this.s2 = s2;
   }

   public void run()
   {
      SynchronizedDemo.print(s1,s2);
   }
}

Die Ausgabe ist nicht vollkommen determiniert. Die A's und die B's werden in Blöcken am Ende auftreten, alles andere kann man nicht vorhersagen.Hier zwei Durchläufe des Programms.

12212121212121122112BBBBBBBBBBAAAAAAAAAA
12121212212112121212AAAAAAAAAABBBBBBBBBB

Hier erhebt sich natürlich die Frage: Was passiert mit dem Lock während der Thread schläft?. Da es die sleep()-Phasen auch im synchronisierten Bereich gibt wird der Lock nicht zurückgegeben. Sonst würde ja der zweite Thread darauf zugreifen und die Ausgaben würden sich mischen. Die folgenden Tabelle klärt das für alle threadrelevanten Methoden.


Das Verhalten von join(), notify(), sleep(), wait() und yield() in Bezug auf Locks

Es ist wichtig zu wissen, welche Methoden beim Aufruf einen Lock zurückgeben oder nicht.

Lockverhalten der threadrelevanten Methoden
MethodeKlasseLockverhalten
join() Thread Behält den Lock
sleep() Thread Behält den Lock
yield() ThreadBehält den Lock
notify() Object Behält den Lock
wait() ObjectGibt den Lock zurück

Merkregel: Nur wait() gibt den Lock zurück.


Mit synchronized vergeben Locks sind reentrant (wiedereintritssfähig)

Was passiert, wenn ein Thread im Besitz eines Locks ist und einen weiteren Bereich betreten will, der mit demselben Lock geschützt ist. Er wird einen Lock anfordern, aber er kann ja keinen mehr bekommen. Kann sich der Monitor merken, an wen er den Lock gegeben hat. Kann er sagen, du hast ja den Lock schon, verwende ihn. Wir betrachten die folgende Situation.

public class SynchronizeIsReentrant
{
  public synchronized first()
  {
    //do something
    second();
    //do something
    second();
    //do something
  }

  public synchronized second()
  {
    //do something
  }
}

Beim Eintritt in die Methode second() fordert der ausführende Thread erneut einen Lock vom Monitor. Nun gibt es zwei Möglichkeiten wie der Monitor reagieren kann. Dazu stellen wir uns den Monitor mal als sprechendes Wesen vor.

Erste Möglichkeit:

Monitor sagt: Ich habe keinen Lock mehr (basta).

Zweite Möglichkeit:

Monitor sagt: Du hast doch schon den Lock, er gilt auch für die neue Situation.

In der ersten Situation spricht man von einem nichtreentranten Lock, die zweite Situation nennt man wie zu erwarten reentrant. Ein reentranter Lock ist so etwas wie ein Generalschlüssel, er sperrt überall. Wäre synchronized nicht reentrant implementiert, so käme es hier zu einem Deadlock da ja der Monitor nur einen Lock hat.


Ein Thread sperrt alle synchronized Methoden

Erhält ein Thread T1 etwa Zugriff auf eine synchronized Methode m1, erhält er den Lock und kann so die Methode exklusiv abarbeiten. Weil es aber nur einen einzigen Lock pro Objekt gibt verhindert er damit aber zwangsläufig den den Zugriff auf alle anderen synchronized Methoden. Kein Thread hat deswegen Zugriff auf irgendeine andere synchronized Methode solange T1 mit m1 beschäftigt ist. Das war einer der Gründe für die Neuentwicklung der Klasse Vector, in der zahlreiche Methoden synchronized sind und die daher alles andere als performant ist. In der Klasse ArrayList verzichtete man daher auf "synchronized". Dafür gibt es in der Utilityklasse Collections eine statische Methode um eine List
nachträglich zu synchronisieren: List list = Collections.synchronizedList(new ArrayList());

Das folgende Beispiel demonstriert dieses Verhalten

Eine Klasse MethodMagazine hat drei identisch aufgebaute synchronized Methoden. Diese melden sich mit einem Zeitstempel, wenn sie aufgerufen werden. Jede Methode dauert 5 Sekunden, was mit einem sleep() Aufruf erreicht wird, denn sleep() gibt den Lock nicht zurück. Drei Threads erhalten ein einziges Objekt dieser Klasse. Jeder Thread ruft genau eine der drei Methoden, aber eben verschiedene. Die Startaufrufe kommen direkt hintereinander. Egal welcher Thread starten kann blockiert er damit 5 Sekunden den Zugriff auf die beiden anderen Methoden. Da jede Methode mit einer Ausgabe des Zeitstempels beginnt sieht man genau, wie die restlichen Methoden blockiert werden.

Die Klasse MethodMagazine

class MethodMagazine
{
  public synchronized void syncMethod_1()
  {
    System.out.println("syncMethod_1() " + new Timestamp(System.currentTimeMillis()) );
    try { TimeUnit.SECONDS.sleep(5);}
    catch(InterruptedException ex) { ex.printStackTrace();  }
  }

  public synchronized void syncMethod_2()
  {
    System.out.println("syncMethod_2() " + new Timestamp(System.currentTimeMillis()));
    try { TimeUnit.SECONDS.sleep(5);}
    catch(InterruptedException ex) { ex.printStackTrace();  }
  }

  public synchronized void syncMethod_3()
  {
    System.out.println("syncMethod_3() " + new Timestamp(System.currentTimeMillis()));
    try { TimeUnit.SECONDS.sleep(5);}
    catch(InterruptedException ex) { ex.printStackTrace();  }
  }
}

Die drei Threads

class Task1 implements Runnable
{
  private MethodMagazine mm = null;

  public Task1(MethodMagazine mm)
  {
    this.mm = mm;
  }

  public void run()
  {
    mm.syncMethod_1();
  }
}

class Task2 implements Runnable
{
  private MethodMagazine mm = null;

  public Task2(MethodMagazine mm)
  {
    this.mm = mm;
  }
  public void run()
  {
    mm.syncMethod_2();
  }
}

class Task3 implements Runnable
{
  private MethodMagazine mm = null;

  public Task3(MethodMagazine mm)
  {
    this.mm = mm;
  }
  public void run()
  {
    mm.syncMethod_3();
  }
}

Die main-Klasse

public class SynchronizedDemo
{
  public static void main(String[] args)
  {
    System.out.println("main");
    MethodMagazine mm = new MethodMagazine();

    Thread th1 = new Thread(new Task1(mm));
    Thread th2 = new Thread(new Task2(mm));
    Thread th3 = new Thread(new Task3(mm));

    th1.start();
    th2.start();
    th3.start();
  }

  System.out.println("end main");

  }  // end main

}  // end class

Einige Abläufe

main
end main
syncMethod_1() 2020-03-11 10:06:11.055
syncMethod_3() 2020-03-11 10:06:16.064
syncMethod_2() 2020-03-11 10:06:21.065

main
end main
syncMethod_1() 2020-03-11 10:07:04.475
syncMethod_3() 2020-03-11 10:07:09.484
syncMethod_2() 2020-03-11 10:07:14.485

main
end main
syncMethod_1() 2020-03-11 10:07:33.726
syncMethod_2() 2020-03-11 10:07:38.737
syncMethod_3() 2020-03-11 10:07:43.737


Ein möglicher Ausweg: Mit verschiedenen Objekten locken

Wir arbeiten nicht mehr mit synchronized Methoden, sondern mit synchronized Blöcken in den Methoden. Dadurch können wir verschiedene Objekt zum locken verwenden. Die Objekt werden über den Konstruktor der Taskklassen injiziert. Im folgenden Beispiel erhalten zwei Tskklassen daselbe Objekt, der dritte Task jedoch ein anderes.

Die neue Klasse MethodMagazine

class MethodMagazine
{
  public void syncMethod_1(Object ob)
  {
    synchronized(ob)
    {
      System.out.println("syncMethod_1() begin " + new Timestamp(System.currentTimeMillis()) );
      try { TimeUnit.SECONDS.sleep(5);}
      catch(InterruptedException ex) { ex.printStackTrace();  }
      System.out.println("syncMethod_1() end   " + new Timestamp(System.currentTimeMillis()) );
    }
  }

  public void syncMethod_2(Object ob)
  {
    synchronized(ob)
    {
      System.out.println("syncMethod_2() begin " + new Timestamp(System.currentTimeMillis()));
      try { TimeUnit.SECONDS.sleep(5);}
      catch(InterruptedException ex) { ex.printStackTrace();  }
      System.out.println("syncMethod_2() end   " + new Timestamp(System.currentTimeMillis()));
    }
  }

  public void syncMethod_3(Object ob)
  {
    synchronized(ob)
    {
      System.out.println("syncMethod_3() begin " + new Timestamp(System.currentTimeMillis()));
      try { TimeUnit.SECONDS.sleep(5);}
      catch(InterruptedException ex) { ex.printStackTrace();  }
      System.out.println("syncMethod_3() end   " + new Timestamp(System.currentTimeMillis()));
    }
  }
}

Die drei Threads die nun ein Objekt über den Konstruktor erhalten


class Task1 implements Runnable
{
  private MethodMagazine mm = null;
  private Object monitor;

  public Task1(MethodMagazine mm, Object monitor)
  {
    this.mm = mm;
    this.monitor = monitor;
  }

  public void run()
  {
    mm.syncMethod_1(monitor);
  }
}


class Task2 implements Runnable
{
  private MethodMagazine mm = null;
  private Object monitor;

  public Task2(MethodMagazine mm, Object monitor)
  {
    this.mm = mm;
    this.monitor = monitor;
  }

  public void run()
  {
    mm.syncMethod_2(monitor);
  }
}


class Task3 implements Runnable
{
  private MethodMagazine mm = null;
  private Object monitor;

  public Task3(MethodMagazine mm, Object monitor)
  {
    this.mm = mm;
    this.monitor = monitor;
  }

  public void run()
  {
    mm.syncMethod_3(monitor);
  }
}

Die main-Klasse


/**
 * Ein Lock sperrt alles !
 * aber hier: verschiedene Objekte für verschiedene Methoden
 */
public class SynchronizedDemo
{
  public static void main(String[] args)
  {
    System.out.println("main");
    MethodMagazine mm = new MethodMagazine();
    Object monitor1 = new Object();
    Object monitor2 = new Object();
    Object monitor3 = monitor1;
    Thread  th1 = new Thread(new Task1(mm, monitor1));
    Thread  th2 = new Thread(new Task2(mm, monitor2));
    Thread  th3 = new Thread(new Task3(mm, monitor3));

    th1.start();
    th2.start();
    th3.start();

    System.out.println("end main");

  }  // end main

}  // end class

Einige Abläufe


main
end main
syncMethod_1() begin 2020-03-11 12:19:41.616
syncMethod_2() begin 2020-03-11 12:19:41.616
syncMethod_1() end   2020-03-11 12:19:46.625
syncMethod_2() end   2020-03-11 12:19:46.625
syncMethod_3() begin 2020-03-11 12:19:46.625
syncMethod_3() end   2020-03-11 12:19:51.626

main
end main
syncMethod_2() begin 2020-03-11 12:20:30.581
syncMethod_1() begin 2020-03-11 12:20:30.581
syncMethod_1() end   2020-03-11 12:20:35.591
syncMethod_2() end   2020-03-11 12:20:35.591
syncMethod_3() begin 2020-03-11 12:20:35.591
syncMethod_3() end   2020-03-11 12:20:40.592

main
end main
syncMethod_1() begin 2020-03-11 12:21:18.396
syncMethod_2() begin 2020-03-11 12:21:18.396
syncMethod_2() end   2020-03-11 12:21:23.405
syncMethod_1() end   2020-03-11 12:21:23.405
syncMethod_3() begin 2020-03-11 12:21:23.405
syncMethod_3() end   2020-03-11 12:21:28.406

main
end main
syncMethod_1() begin 2020-03-11 12:22:01.768
syncMethod_2() begin 2020-03-11 12:22:01.768
syncMethod_1() end   2020-03-11 12:22:06.777
syncMethod_3() begin 2020-03-11 12:22:06.777
syncMethod_2() end   2020-03-11 12:22:06.778
syncMethod_3() end   2020-03-11 12:22:11.778

Dieses Konzept wurde in Java 5 ausgebaut, indem man statt der Klasse Object eine eigene Klasse Lock einführte, die über zusätzliche Möglichkeiten verfügt einen Lock zu konfigurieren.