Ursprungsmitteilung
Thema Threads kompetent einsetzen-1. Teil: synchronized 
Autor Aegidius Plüss 
Eingangsdatum 2006-06-24 21:00:00.0 
Mitteilung (Download: Vollständiger Text (V1.2) als PDF, alle Beispielprogramme)

Schreibt man Programme mit einer grafischen Benützeroberfläche (GUI), wie es heute üblich ist, so ist man unmittelbar mit der Nebenläufigkeit (Multithreading) konfrontiert, denn bei Ereignissen auf der Benützeroberfläche (Button-Betätigungen, usw.) werden Methoden (Callbacks) von einem speziellen Java-Thread aufgerufen (dem Event Dispatch Thread, EDT). Fehlen die nötigen Kenntnisse über das Multithreading und hält man sich nicht an gewisse grundsätzliche Regeln, so können bereits einfache Programme ein fehlerhaftes oder zumindest ungewöhliches Verhalten aufweisen (zeitweises Einfrieren des GUI, usw.). Da es sich oft um Fehler handelt, die nur unter gewissen schlecht reproduzierbaren Umständen auftreten, sind sie grundsätzlich schwierig zu beheben. Wir betrachten im Folgenden eine exemplarische Lernsequenz, welche die Probleme aufdeckt und Lösungen anbietet. Wie so oft, setzen wir aus didaktischen Gründen zu Beginn die Turtle aus dem Package ch.aplu.turtle ein, denn ein Bild sagt bekanntlich mehr als tausend Worte.

Ausgangslage
Eine Turtle soll in einem Applikationsprogramm regelmässige Dreieckszacken zeichnen. Ein etwas faules "Stempelkind", das einen eigenen Thread und damit viel Eigenleben besitzt, wird zu bestimmten Zeiten das Turtlebild auf die Unterlage "stempeln". Nachher schläft es wieder ziemlich lange ein. Wie wir aus den Grundlagen der Threadprogrammierung entnehmen, leiten wir dazu die Klasse StampKid aus Thread ab und implementieren die run-Methode. Beim Start des Threads wird diese im neuen Thread laufen und soll, immer nach einer beträchtlichen Schlafpause, die Turtle stempeln. Bei Programmabbruch mit dem Close-Button wird vom Button-Callback die Methode System.exit() aufgerufen, wodurch auch der Thread des Stempelkindes beendet wird.
Damit das Stempelkind auf die Turtle zugreifen kann, um sie zu stempeln, übergeben wir ihm bei der Konstruktion eine Referenz der Applikationsklasseninstanz. Weil die Instanzvariable Turtle t keinen expliziten Zugriffsbezeichner besitzt ist, kann das Stempelkind sie benützen, da es sich im gleichen Package befindet.
Im Applikationsprogramm wird die Turtle zuerst in eine günstige Anfangsposition gesetzt. Mit dem Aufruf von start() wird der Thread des Stempelkindes seine Arbeit aufnehmen, indem er im neu erzeugten Thread die run-Methoden ausführt. In einer Endlosschleife zeichnet die Turtle einen Dreieckszacken nach dem anderen.

// SynchEx1.java
// StampKid stamps whenever it likes

import ch.aplu.turtle.*;

public class SynchEx1
{
  Turtle t = new Turtle();

  public SynchEx1()
  {
    t.speed(100);
    t.setPos(-1800);
    t.right(10);
    new StampKid(this).start();
    while (true)
    {
      t.forward(100);
      t.right(160);
      t.forward(100);
      t.left(160);
    }
  }

  public static void main(String[] args)
  {
    new SynchEx1();
  }
}

// ---------------------- class StampKid ---------------------
class StampKid extends Thread
{
  SynchEx1 app;

  StampKid(SynchEx1 app)
  {
    this.app = app;
  }

  public void run()
  {
    while (true)
    {
      app.t.stampTurtle();
      try
      {
        Thread.currentThread().sleep(5000);
      }
      catch (InterruptedException ex) {}
    }
  }
}

Resultat
Das Resultat erstaunt wenig: das Stempelkind erwacht alle 5 Sekunden und stempelt die Turtle, ohne Rücksicht darauf, wo sich diese gerade befindet, sogar mitten in einer Drehung. Das Resultat ist entsprechend unkoordiniert, da der Applikationsthread und der Thread des Stempelkindes nicht synchronisiert sind.

Kommentar
Wir möchten das Stempelkind veranlassen, nur dann zu stempeln, wenn ein Dreieckszacken fertig gezeichnet ist. Dazu versuchen wir den Kindthread daran zu hindern, den Schleifenblock zu unterbrechen. Nach einer allgemeinen Lehrmeinung genügt es dazu, den betreffenden Block als synchronized zu bezeichnen. Wir fügen daher im ersten Versuch einzig einen synchronized-Block ein:

while (true)
{
  synchronized(this)
  {
    t.forward(100);
    t.right(160);
    t.forward(100);
    t.left(160);
  }
}

Resultat
Das Resultat ist ernüchternd, wir stellen keinen Unterschied des Programmverhaltens fest.

Kommentar
Offenbar ist es ein Irrtum zu glauben, man könne einen Programmblock mit synchronized bezeichnen, um zu verhindern dass er von einem anderen Thread unterbrochen wird. Diese Vorstellung stammt möglicherweise aus den Zeiten der kooperativen Betriebsysteme, wo jedes Programm (bis auf Interrupts) den Code in alleiniger Herrschaft ausführen konnte und selbst entschied, ob es anderen Programmen die Ausführung gestattete. Es genügte damals, beim Eintritt in einen Codeteil, der nicht unterbrochen werden durfte, mit einem Befehl wie noyield() die Weitergabe abzuschalten. Diese Zeiten sind aber eindeutig passé. In modernen Multitasking-Systemen werden Prozesse und Threads vom Betriebssystem preemptive verwaltet, d.h. die Umschaltung erfolgt ohne viele Einflussmöglichkeiten durch das Anwenderprogramm (unter Windows werden Prozess-Prioritäten sehr schlecht unterstützt). Es lassen sich daher keine Programmblöcke mehr auszeichnen, die vor Unterbrechungen gänzlich geschützt sind. Man beschränkt sich vielmehr darauf, gemeinsam benützte Daten derart zu schützen, dass mehrere Threads koordiniert oder eben synchronisiert darauf zugreifen. Unter Daten versteht man hier Java-Klasseninstanzen, deren Instanzvariablen man durch geeignete Verfahren vor einem ungeordneten Zugriff (lesen oder verändern) durch verschiedene Threads schützen will.
Java stellt dazu das Schlüsselwort synchronized zur Verfügung. Mit ihm werden Programmblöcke oder ganze Methoden in mehreren verschiedenen Threads ausgezeichnet, die koordiniert auf ein und dasselbe Objekt zugreifen wollen. Die betreffende Objektreferenz wird synchronized als Parameter mitgegeben. Es handelt sich also nicht um einen einseitigen Schutz vor Unterbrechungen, sondern vielmehr um eine gegenseitige Vereinbarung über den geordneten Zugriff auf ein Objekt. Der Parameter kann auch this sein, wenn man die aktuelle Instanz und damit alle seine Instanzvariablen schützen will. Deklariert man eine ganze Methode als synchronized, entspricht dies einem synchronized(this) des ganzen Methodenrumpfs.
Um unsere Zielsetzung zu erreichen, müssen wir auch im Thread des Stempelkinds den Bereich, der auf die Applikationsinstanz zugreift, in einen synchronized-Block setzen.

public void run()
{
  while (true)
  {
    synchronized(this)
    {
      app.t.stampTurtle();
    }
    try
    {
      Thread.currentThread().sleep(5000);
    }
    catch (InterruptedException ex) {}
  }
}

Resultat
Offensichtlich haben wir etwas Grundsätzliches falsch gemacht, denn das Resultat ist unverändert und das Stempelkind kann den Zackencode an beliebigen Stellen unterbrechen. Wo liegt der Fehler?

Kommentar
Wir haben in der Eile übersehen, dass die Objektreferenz, die wir synchronized mitgeben, das von beiden Threads zu schützende Objekt bezeichnet. Setzen wir in beiden Threads synchronized(this), so werden dabei zwei verschiedene Klasseninstanzen referenziert, nämlich eine Instanz der Applikationsklasse und eine Instanz des Stempelkindes. Mit dieser Erkenntnis schreiben wir das korrekte Programm, indem wir auch im Stempelkind mit synchronized(app) die Applikationsklasse referenzieren.

// SynchEx2.java
// Finally it works!
// The synchronized block is now atomic (not interrupted)

import ch.aplu.turtle.*;

public class SynchEx2
{
  Turtle t = new Turtle();

  public SynchEx2()
  {
    t.speed(100);
    t.setPos(-1800);
    t.right(10);
    new StampKid(this).start();
    while (true)
    {
      synchronized(this)
      {
        t.forward(100);
        t.right(160);
        t.forward(100);
        t.left(160);
      }
    }
  }

  public static void main(String[] args)
  {
    new SynchEx2();
  }
}

// ---------------------- class StampKid ---------------------
class StampKid extends Thread
{
  SynchEx2 app;

  StampKid(SynchEx2 app)
  {
    this.app = app;
  }

  public void run()
  {
    while (true)
    {
      synchronized(app)
      {
        app.t.stampTurtle();
      }
      try
      {
        Thread.currentThread().sleep(5000);
      }
      catch (InterruptedException ex) {}
    }
  }
}

Resultat
Es wird jetzt nur noch an gewünschter Stelle gestempelt, leider allerdings nicht nach jedem Zacken.

Kommentar
Java implementiert den gegenseitigen Ausschluss mit synchronized wie folgt: Beim Eintritt in den mit synchronized(obj) bezeichneten Block (auch kritischer Abschnitt kA genannt) erhält der ausführende Thread vom Objekt obj eine Sperre (Lock, auch Monitor genannt). Den Monitor kann man sich auch als eine Art Schlüssel vorstellen, von dem es für jede Objektinstanz nur einen einzigen gibt. Die anderen Threads, welche etwas später ebenfalls in den kA eintreten wollen, müssen sich zuerst diesen Monitor beschaffen. Ist dieser bereits vergeben, so werden sie vom Threadscheduler in einen blockierten Zustand versetzt. Sobald der Monitor beim Verlassen des kA frei wird, gehen die blockierten Threads in den runnable-Zustand und versuchen, vom Threadscheduler den Monitor zu erhalten. Es ist nicht zum vornherein klar, welcher Thread diesen als ersten auch tatsächlich erhält und laufen wird. Es können aber alle blockierten Threads sicher sein, den Monitor zu kriegen, bevor der erste Thread wieder an die Reihe kommt, denn der Scheduler hält sich an das Prinzip der Fairness.
Das Verhalten mit mehreren Threads, die sich um den Monitor bemühen, können wir untersuchen, wenn wir zusätzlich zum Stempelkind noch ein Malkind deklarieren, das die Turtle rot oder grün färben kann. Dazu verwenden wir den Thread PaintKid, der in der run-Methode mit setColor() die Farbe der Turtle verändert.

// SynchEx3.java
// If two threads are waiting, it's not sure who will be first

import ch.aplu.turtle.*;
import java.awt.Color;

public class SynchEx3
{
  Turtle t = new Turtle();

  public SynchEx3()
  {
    t.speed(100);
    t.setPos(-1800);
    t.right(10);
    t.setColor(Color.green);
    new PaintKid(this).start();
    new StampKid(this).start();
    while (true)
    {
      synchronized(t)
      {
        t.forward(100);
        t.right(160);
        t.forward(100);
        t.left(160);
      }
    }
  }

  public static void main(String[] args)
  {
    new SynchEx3();
  }
}

// ---------------------- class StampKid ---------------------
class StampKid extends Thread
{
  SynchEx3 app;

  StampKid(SynchEx3 app)
  {
    this.app = app;
  }

  public void run()
  {
    while (true)
    {
      synchronized(app.t)
      {
        app.t.stampTurtle();
      }
      try
      {
        Thread.currentThread().sleep(1000);
      }
      catch (InterruptedException ex) {}
    }
  }
}

// ---------------------- class PaintKid ---------------------
class PaintKid extends Thread
{
  SynchEx3 app;

  PaintKid(SynchEx3 app)
  {
    this.app = app;
  }

  public void run()
  {
    while (true)
    {
      synchronized(app.t)
      {
        if (app.t.getColor() == Color.red)
          app.t.setColor(Color.green);
        else
          app.t.setColor(Color.red);
      }
      try
      {
        Thread.currentThread().sleep(1300);
      }
      catch (InterruptedException ex) {}
    }
  }
}

Resultat
Die Turtle startet grün und wird nach der ersten und zweiten Kehrtwendung rot gestempelt. Offensichtlich hat bei der ersten Kehrtwendung das Malkind, bei der zweiten aber das Stempelkind Vorrang.

wait und notify
Zuletzt bleibt uns noch das Problem zu lösen, dafür zu sorgen, dass die Turtle nach jedem Zacken gestempelt wird. Offensichtlich muss der Applikationsthread auf das Aufwachen des Langschläfer-Stempelkinds warten, bevor er mit dem Zeichen des nächsten Zacken weiterfährt. Dazu stellt Java die Methoden wait() und notify() der Klasse Object zur Verfügung, die daher von allen Instanzen aufrufbar sind. Dabei müssen wir beachten, dass wait() nur von einem Objekt aufgerufen werden darf, das den Monitor vergeben hat, sich also in einem kA befindet, und dass notify() des gleichen Objekt aufgerufen werden muss, um den Thread weiter laufen zu lassen. Sobald wait() aufgerufen wird, geht der Thread in den Zustand suspended und gibt den Monitor vorübergehend ab. Mit notify() wird der Thread später genau an dieser Stelle weiter laufen.

// SynchEx4.java
// Wait for StampKid, to be sure, we are stamped

import ch.aplu.turtle.*;

public class SynchEx4
{
  Turtle t = new Turtle();

  public SynchEx4()
  {
    t.speed(100);
    t.setPos(-1800);
    t.right(10);
    new StampKid(this).start();
    while (true)
    {
      synchronized(this)
      { // We owe the monitor now
        t.forward(100);
        t.right(160);
        t.forward(100);
        t.left(160);
        try
        {
          wait();  // Suspend, until we get notified
        }
        catch (InterruptedException ex) {}
      }
    }
  }

  public static void main(String[] args)
  {
    new SynchEx4();
  }
}

// ---------------------- class StampKid ---------------------
class StampKid extends Thread
{
  SynchEx4 app;

  StampKid(SynchEx4 app)
  {
    this.app = app;
  }

  public void run()
  {
    while (true)
    {
      synchronized(app)
      {
        app.t.stampTurtle();
        app.notify();  // Notify the suspended app
      }
      try
      {
        Thread.currentThread().sleep(5000);
      }
      catch (InterruptedException ex) {}
    }
  }
}

Resultat
Wir haben endlich erreicht, was wir ursprünglich beabsichtigten und die Turtle wird nach jedem Zacken gestempelt.

Kommentar
Das Zeichnen des Zackens erfolgt in einem Block, der durch den Stempelthread nicht unterbrochen werden kann. Dasselbe würden wir erreichen, falls wir eine synchronisierte Methode zacken() deklarieren. Die Änderung betrifft folgenden Programmteil:

public SynchEx4()
{
  t.speed(100);
  t.setPos(-180, 0);
  t.right(10);
  new StampKid(this).start();
  while (true)
    zacken();
}

synchronized void zacken()
{
  t.forward(100);
  t.right(160);
  t.forward(100);
  t.left(160);
  try
  {
   wait();
  }
  catch (InterruptedException ex) {}
}

Wir haben nun allerdings des Guten zu viel getan, denn wir müssen eigentlich gar nicht die ganze Applikationsinstanz, sondern lediglich die Turtleinstanz vor dem gemeinsamen Zugriff schützen. Es ist aber leicht, das Programm entsprechend zu modifizieren. Das Resultat bleibt das gleiche.

// SynchEx5.java
// Protect Turtle only

import ch.aplu.turtle.*;

public class SynchEx5
{
  Turtle t = new Turtle();

  public SynchEx5()
  {
    t.speed(100);
    t.setPos(-1800);
    t.right(10);
    new StampKid(t).start();
    while (true)
    {
      synchronized(t)
      {
        t.forward(100);
        t.right(160);
        t.forward(100);
        t.left(160);
        try
        {
          t.wait();
        }
        catch (InterruptedException ex) {}
      }
    }
  }

  public static void main(String[] args)
  {
    new SynchEx5();
  }
}

// ---------------------- class StampKid ---------------------
class StampKid extends Thread
{
  Turtle t;

  StampKid(Turtle t)
  {
    this.t = t;
  }

  public void run()
  {
    while (true)
    {
      synchronized(t)
      {
        t.stampTurtle();
        t.notify();
      }
      try
      {
        Thread.currentThread().sleep(5000);
      }
      catch (InterruptedException ex) {}
    }
  }
}

 
 
      
Antworten
Thema synchronized - anschauliches Beispiel 
Autor Aegidius Plüss 
Eingangsdatum 2007-08-13 12:06:21.0 
Mitteilung Das Beispiel zeigt, dass die OOP die Umwelt gut modellieren kann.

Umgangssprache:
Ein Auftraggeber will eine Fahne mit zwei konzentrisch angeordneten Sternkreisen fabrizieren lassen. Dazu stellt er zwei Maler an, die den äußeren und inneren Sternkreis miteinander malen.

OOP:
Die Applikation erzeugt ein Grafikfenster-Objekt und zwei Threads. Nach dem Start führt die run-Methode die Arbeit durch.

Ohne Synchronisierung der beiden Threads ergibt sich ein Durcheinander. Um dies zu vermeiden, gibt der Chef (Threadmanager) dem Maler vor dem Zeichnen eines Sterns einen Pinsel (eine 'Sperre') ab, ohne den er nicht malen kann. Er behält ihn, bis er einen einzelnen Stern fertig gezeichnet hat. Der andere Maler muss warten, bis er den Pinsel vom Chef erhält. Dieser ist 'fair' und gibt die Pinsel abwechslungsweise an die Maler.

Implementierung:

// StarFlag.java
// Comment out 'synchronized' and you see the mess

import ch.aplu.util.*;
import java.awt.Color;

// -------------------- Application class ---------------
public class StarFlag
{
  // -------------------- class Painter -------------------
  private class Painter extends Thread
  {
    private double radius;
    private double size;
    private Color color;

    Painter(double radius, double size, Color color)
    {
      this.radius = radius;
      this.size = size;
      this.color = color;
    }

    private void drawStar(double x, double y, double r, Color color)
    {
      synchronized(brush)  // Acquire lock on brush object
      {
        Color oldColor = color;
        bunting.color(color);
        bunting.fillTriangle(- Math.sqrt(3)/2 * r, y - r/2,
                             x + Math.sqrt(3)/2 * r, y - r/2,
                             x, y + r);
        Console.delay(10);  // Relax a moment
        bunting.fillTriangle(- Math.sqrt(3)/2 * r, y + r/2,
                             x + Math.sqrt(3)/2 * r, y + r/2,
                             x, y - r);
        bunting.color(oldColor);
      }
    }

    public void run()
    {
      double x;
      double y;
      for (int i = 0; i < 12; i++)
      {
        x = 0.5 + radius * Math.cos(Math.toRadians(360/12) * i);
        y = 0.5 + radius * Math.sin(Math.toRadians(360/12) * i);
        drawStar(x, y, size, color);
        Console.delay((int)(10000*size));  // Big size - long work
      }
    }
  }

  // Create bunting
  private GPanel bunting = new GPanel();

  // Create brush for locking
  private Object brush = new Object();

  public StarFlag()
  {
    // Hire painters
    Painter p1 = new Painter(0.30.05Color.RED);   // Outer star circle
    Painter p2 = new Painter(0.20.03Color.GREEN)// Inner star circle

    // Let 'em do the job
    p1.start();
    p2.start();
  }

  public static void main(String[] args)
  {
    new StarFlag();
  }
}