4 Einführung in Java Threads

 
WEITER LESEN
4 Einführung in Java Threads
Praktikum zur Lehrveranstaltung Betriebssysteme                                    Seite 4 - 1

4 Einführung in Java Threads

Die meisten Betriebssysteme erlauben, dass mehrere Programme auf Multiprozessorsystemen
oder Multicore-Prozessorsystemen gleichzeitig (also zeitlich echt parallel) und auf Einprozessor-
systemen quasi-gleichzeitig (zeitlich ineinander verschachtelt, timesharing) ausgeführt werden.
Bei einem Multiprozessor- oder Multicore-Prozessorsystem werden die Programme auf den ein-
zelnen Prozessoren oder Prozessorkernen im Allgemeinen auch noch zeitlich ineinander ver-
schachtelt, um eine möglichst hohe Leistungsfähigkeit und Fairness zu erreichen. Diese Konzep-
te wurden auch auf eine Parallelität innerhalb eines Prozesses erweitert, z. B. Coroutinen in den
Programmiersprachen „Simula“ (process) oder „ADA“ (task). Im Betriebssystem Mach wurden
in der zweiten Hälfte der 1980-er Jahre C-Threads eingeführt und untersucht, die die Parallelität
innerhalb eines Programmes auch auf Betriebssystemebene unterstützen. Die bekanntesten
Schnittstellen zur Thread-Programmierung in der Programmiersprache C sind die standardisier-
ten „POSIX-Threads“ (pthreads), die von den meisten Betriebssystemen unterstützt werden und
die „Win32-Threads“ der Firma Microsoft für ihre Windows-Betriebssysteme. Die Programmier-
sprache Java stellt ebenfalls eine Thread-Schnittstelle zur Verfügung, die, wie Java selbst, pro-
zessorarchitektur- und betriebssystemübergreifend ist. Java-Threads funktionieren auch dann,
wenn das Betriebssystem keine Threads unterstützt, da die entsprechende Funktionalität in
diesem Fall in der virtuellen Maschine von Java zur Verfügung gestellt wird.
Warum brauchen wir überhaupt Threads? Schauen wir uns einfach ein kleines Programm an, in
dem ein Ball in einer Zeichenfläche bewegt wird. Wenn der Ball an eine Kante stößt, wird er dort
reflektiert und setzt seine Bahn in einer anderen Richtung fort (übersetzen und starten Sie das
Programm MoveBallMain.java im Verzeichnis prog/Thread/MoveBall des Programmarchivs;
im Folgenden befinden sich alle Verzeichnisse unterhalb des Verzeichnisses prog/Thread).

Wenn Sie mehrfach auf „Start blue ball“ klicken, passiert erst einmal nichts. Wenn der erste Ball
seine Bahn beendet hat, startet der zweite Ball usw. Damit Sie sehen, dass ein neuer Ball gestar-
tet wurde, bleibt die letzte Position des „alten“ Balls auf dem Bildschirm erhalten. Wenn Sie auf

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
4 Einführung in Java Threads
Praktikum zur Lehrveranstaltung Betriebssysteme                                     Seite 4 - 2

„Close“ klicken, passiert zunächst wieder nichts. Das Fenster wird erst dann geschlossen, wenn
der letzte Ball seine Bahn beendet hat. Eigentlich sollten mehrere Bälle parallel über die Zeichen-
fläche „fliegen“ und das Fenster auch sofort geschlossen werden, wenn wir auf „Close“ klicken.
Diese Parallelität können wir mit Hilfe von Java-Threads implementieren.
Ein Thread kann zum Beispiel dadurch erzeugt werden, dass die Klasse Thread aus java.lang
erweitert (public class Thread extends Object implements Runnable) oder die Schnittstelle
Runnable aus java.lang implementiert (public interface Runnable) wird. Falls die Klasse, die als
Thread laufen soll, bereits von einer anderen Klasse erbt, muss „Runnable“ implementiert wer-
den, da Java keine Mehrfachvererbung erlaubt. In jedem Fall muss die Methode „run ()“ über-
schrieben werden, die dann das eigentliche Programm des Threads enthält. Die Methode „run ()“
darf niemals direkt aufgerufen werden, da die Methode dann nicht parallel zur aufrufenden
Methode ausgeführt wird, sondern sequentiell innerhalb der Methode abläuft (quasi wie ein
Unterprogrammaufruf). Ein Thread wird immer mit der Methode „start ()“ gestartet, die die
Thread-Umgebung initialisiert, danach die weitere Ausführung an die Methode „run ()“ überträgt
und sich dann beendet, sodass der neue Thread jetzt parallel zum Erzeuger des Threads abläuft.
Das folgende Programm im Verzeichnis HelloThread des Programmarchivs erweitert
beispielsweise die Klasse Thread.
/* Program creates a thread which prints a message on the screen.
 * ...
 * File: HelloOneMain.java              Author: S. Gross
 */

public class HelloOneMain
{
  public static void main (String args[])
  {
    HelloThread hello = new HelloThread ();
    hello.start ();
  }
}

Im Hauptprogramm wird nur ein neues Thread-Objekt erzeugt und gestartet. Danach wartet das
Hauptprogramm automatisch darauf, dass der Thread endet, bevor es sich selbst beendet.
/* This thread displays a message on the screen.
  * ...
  * File: HelloThread.java              Author: S. Gross
  */
public class HelloThread extends Thread
{
   private int threadID;

  /* constructors                                                                          */
  public HelloThread ()
  {
    this.threadID = 0;
    setName ("HelloThread-" + Integer.toString (threadID));
  }

  public HelloThread (int i)
  {
    this.threadID = i;
    setName ("HelloThread-" + Integer.toString (threadID));
  }

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
4 Einführung in Java Threads
Praktikum zur Lehrveranstaltung Betriebssysteme                                   Seite 4 - 3

    /* implementation of method "run"                                                    */
    public void run ()
    {
      System.out.println (this.toString () + ": Hello!");
    }

    /* overloading "toString ()"                                                         */
    public String toString ()
    {
      return getName ();
    }
}

Im Konstruktor der Klasse HelloThread wird ein Name für den Thread mit Hilfe der Methode
„setName ()“ der Klasse Thread erzeugt. Wenn der Aufruf dieser Methode fehlt, wird vom
System automatisch ein globaler Name der Form „Thread-n“ vergeben, wobei „n“ eine ganze
Zahl ist. Außerdem wird in der Klasse die Methode „toString ()“ überschrieben, sodass nur noch
der Name des Threads ausgegeben wird (standardmäßig würde die Methode den Namen, die
Priorität und die Gruppe des Threads ausgeben). Wie Sie der Methode „run ()“ entnehmen
können, gibt der Thread nur seinen Namen und „Hello!“ aus. Die Dateien „HelloTwoMain.java“
und „HelloThreeMain.java“ zeigen, wie Sie mehrere Threads erzeugen können.
/* Program creates some threads which print messages on the screen.
 * ...
 * File: HelloThreeMain.java            Author: S. Gross
 */

public class HelloThreeMain
{
  public static void main (String args[])
  {
    final int NUMBER_OF_THREADS = 4;

        HelloThread hello[] = new HelloThread [NUMBER_OF_THREADS];

        for (int i = 0; i < NUMBER_OF_THREADS; ++i)
        {
          hello[i] = new HelloThread (i);
          hello[i].start ();
        }
    }
}

Sie sollten sich immer die Identifikatoren der Threads merken, damit Sie ggf. spezielle Metho-
den für einen Thread aufrufen können (z. B. „interrupt ()“, um einen Thread zu beenden). Sie
müssen auch immer bestimmte Werte als Konstante definieren (Programmierrichtlinien!), damit
sie ggf. an einer zentralen Stelle geändert werden können und nicht an mehreren Stellen geändert
werden müssen (z. B. NUMBER_OF_THREADS). Das Erzeugen und Starten eines Threads kann
auch in einer Anweisung erfolgen. Im Programm „HelloFourMain.java“ wird die start-Methode
direkt an die Zuweisung angefügt ((hello[i] = new HelloThread (i)).start ();) und im Programm
„HelloFiveMain.java“ wird ein Thread erzeugt, dessen Identifikator nicht gespeichert wird ((new
HelloThread (i)).start ();), sodass mit ihm später nicht kommuniziert werden kann.
Im Verzeichnis HelloRunnable finden Sie dieselben Programme in der Version, die die Schnitt-
stelle Runnable implementiert. In diesem Fall benötigen Sie einen Thread-Konstruktor, der aus

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
4 Einführung in Java Threads
Praktikum zur Lehrveranstaltung Betriebssysteme                                   Seite 4 - 4

einem Runnable einen Thread erzeugt. Die Klasse Thread stellt die folgenden Konstruktoren zur
Verfügung.
1)   Thread (ThreadGroup group, Runnable target, String name)
     Erzeugt einen neuen Thread, der target ausführt, den Namen name hat und zur Gruppe
     group gehört. Falls die Gruppe null ist, ist der Thread im Allgemeinen in derselben Gruppe
     wie der erzeugende Thread (hängt davon ab, ob es einen sogenannten Security Manager
     gibt). Falls target den Wert null hat, muss die Methode „run ()“ in diesem Thread imple-
     mentiert worden sein, damit der Thread etwas macht (class ... implements Runnable {...}).
     Falls target einen Wert ungleich null hat, wird die Methode „run ()“ von target aufgerufen.
     Die Priorität des neuen Threads entspricht der Priorität des erzeugenden Threads (kann mit
     der Methode „setPriority ()“ der Klasse Thread geändert werden).
2)   Thread ()
     Entspricht Thread (null, null, gname), wobei gname ein neu erzeugter Name der Form
     Thread-n ist (n ist eine ganze Zahl).
3)   Thread (Runnable target)
     Entspricht Thread (null, target, gname), wobei gname ein neu erzeugter Name der Form
     Thread-n ist (n ist eine ganze Zahl).
4)   Thread (Runnable target, String name)
     Entspricht Thread (null, target, name).
5)   Thread (String name)
     Entspricht Thread (null, null, name).
6)   Thread (ThreadGroup group, Runnable target)
     Entspricht Thread (group, target, gname), wobei gname ein neu erzeugter Name der Form
     Thread-n ist (n ist eine ganze Zahl).
7)   Thread (ThreadGroup group, String name)
     Entspricht Thread (group, null, name).
8)   Thread (ThreadGroup group, Runnable target, String name, long stackSize)
     Verhält sich im Wesentlichen genauso wie „Thread (ThreadGroup group, Runnable target,
     String name)“. Sie können allerdings zusätzlich die Größe des Kellerspeichers angegeben.
     Falls die Größe Null ist, entspricht der Aufruf exakt dem anderen Aufruf. Die Implemen-
     tierung dieses Konstruktors ist abhängig von der Plattform. Unter Umständen wird der
     Parameter von der Implementierung überhaupt nicht beachtet! Sie sollten diesen Konstruk-
     tor nicht verwenden.
Im folgenden Hauptprogramm „HelloOneMain.java“ im Verzeichnis HelloRunnable wird
zuerst ein Runnable-Objekt erzeugt, dem man auch wieder einen Namen geben kann. Danach
kann mit dem Runnable-Objekt ein Thread erzeugt und gestartet werden.

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
4 Einführung in Java Threads
Praktikum zur Lehrveranstaltung Betriebssysteme                                 Seite 4 - 5

/* Program creates a thread which prints a message on the screen.
 * This version uses the interface Runnable.
 * ...
 * File: HelloOneMain.java              Author: S. Gross
 */

public class HelloOneMain
{
  public static void main (String args[])
  {
    String threadName = "HelloRunnable";

        /* create runnable object                                                      */
        HelloRunnable helloRun = new HelloRunnable (threadName);
        /* create thread with constructor Thread (target)                              */
        Thread hello = new Thread (helloRun);
        hello.start ();
    }
}

/* This thread displays a message on the screen.
 * ...
 * File: HelloRunnable.java             Author: S. Gross
 */

public class HelloRunnable implements Runnable
{
  private int    threadID;
  private String name;

    /* constructors                                                                    */
    public HelloRunnable ()
    {
      this.threadID = 0;
      this.name     = "HelloRunnable-" + Integer.toString (threadID);
    }

    public HelloRunnable (String name)
    {
      this.threadID = 0;
      this.name     = name + "-" + Integer.toString (threadID);
    }

    public HelloRunnable (int i, String name)
    {
      this.threadID = i;
      this.name     = name + "-" + Integer.toString (threadID);
    }

    /* implementation of method "run"                                                  */
    public void run ()
    {
      System.out.println (name + ": Hello!");
    }
}

Die Programme „HelloTwoMain.java“, „HelloThreeMain.java“ und „HelloFourMain.java“ zei-
gen wieder die verschiedenen Möglichkeiten, wie der Thread in „Kurzform” erzeugt werden
kann. Manchmal kann es sinnvoll sein, dass sich ein Thread selbst startet. Die entsprechenden
Programme befinden sich im Verzeichnis HelloThreadAutoStart.

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
4 Einführung in Java Threads
Praktikum zur Lehrveranstaltung Betriebssysteme                                  Seite 4 - 6

/* Program creates some threads which start themselves.
  * ...
  * File: HelloAutoStartMain.java        Author: S. Gross
  */
public class HelloAutoStartMain
{
   public static void main (String args[])
   {
     final int NUMBER_OF_THREADS = 4;
        String threadName      = "HelloAutoStart";
        HelloAutoStart hello[] = new HelloAutoStart [NUMBER_OF_THREADS];

        System.out.println ("\n" + Thread.currentThread ().getName () +
                            ": create " + NUMBER_OF_THREADS +
                            " threads which start themselves.");
        for (int i = 0; i < NUMBER_OF_THREADS; ++i)
        {
          /* create thread which starts itself                                          */
          hello[i] = new HelloAutoStart (i, threadName);
        }
    }
}

/* This thread starts itself in its constructor.
  * ...
  * File: HelloAutoStart.java           Author: S. Gross
  */
public class HelloAutoStart implements Runnable
{
   private int    threadID;
   private String name;
    /* constructors                                                                     */
    ...
    public HelloAutoStart (int i, String name)
    {
      this.threadID = i;
      this.name     = name + "-" + Integer.toString (threadID);
      /* start thread with constructor "Thread (Runnable target)"                       */
      (new Thread (this)).start ();
    }
    /* implementation of method "run"                                                   */
    public void run ()
    {
      System.out.println (name + ": Hello!");
    }
}

Obwohl „main ()“ ein Thread ist, können die Methoden der Klasse „Thread“ nicht benutzt
werden, da die Thread-Referenz auf „main“ fehlt. Dieser Mangel kann durch die Methode
„currentThread ()“ behoben werden, wie Sie dem obigen Beispiel entnehmen können. Da die
Bestimmung der Thread-Referenz relativ „teuer“ ist, sollte das Ergebnis zwischengespeichert
werden, wenn es häufiger benötigt wird. Nachdem Sie jetzt Threads erzeugen können, müssen
Sie noch wissen, wie Sie einen Thread beenden können, der z. B. in einer Endlosschleife läuft.
Im Allgemeinen wird für den zu beendenden Thread die Methode „interrupt ()“ aufgerufen. Der
Thread soll sich dann selbst so schnell wie möglich beenden, nachdem er ggf. alle privaten
Betriebsmittel freigegeben hat. Die entsprechenden Beispielprogramme befinden sich im
Verzeichnis HelloTerminate.

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                            Seite 4 - 7

/* Program creates a thread which prints a message on the screen.
  * This version uses "interrupt ()" and "Thread.interrupted ()"
  * ...
  * File: HelloOneMain.java               Author: S. Gross
  */
public class HelloOneMain
{
   public static void main (String args[])
   {
     final long WAIT_TIME         = 5000;         /* 5 seconds                   */
     final int NUMBER_OF_THREADS = 4;

        HelloOneThread hello[] = new HelloOneThread [NUMBER_OF_THREADS];

        /* create all threads                                                    */
        for (int i = 0; i < NUMBER_OF_THREADS; ++i)
        {
          hello[i] = new HelloOneThread (i);
          hello[i].start ();
        }

        /* wait some time before terminating all threads                         */
        try
        {
          Thread.sleep (WAIT_TIME);
        } catch (InterruptedException e)
        {
          System.err.println ("HelloOneMain: received unexpected " +
                              "InterruptedException.");
        }

        /* Terminate all threads.                                                */
        for (int i = 0; i < NUMBER_OF_THREADS; ++i)
        {
          System.out.println ("HelloOneMain: I stop thread '" +
                              hello[i].getName () + "' via " +
                              "'interrupt ()'.");
          (hello[i]).interrupt ();
        }

        /* Join all terminating threads                                          */
        for (int i = 0; i < NUMBER_OF_THREADS; ++i)
        {
          String thrName = hello[i].getName ();

            try
            {
              hello[i].join ();
              System.out.println ("HelloOneMain: '" + thrName +
                                "' terminated.");
            } catch (InterruptedException e)
            {
              System.err.println ("HelloOneMain: received unexpected " +
                                  "InterruptedException while joining "+
                                  "'" + thrName + "'.");
            }
        }
    }
}

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                  Seite 4 - 8

Im obigen Programm sehen Sie, dass Sie den Thread-Identifikator benötigen, wenn Sie einen
Thread beenden wollen oder den speziellen Namen eines Threads benutzen wollen. Weiterhin
sehen Sie, wie Sie mit der Methode „join ()“ auf das Ende eines Threads warten können, bevor
andere Aktionen erfolgen (z. B. Dateien parallel aus dem Internet laden und deren Bearbeitung
starten, wenn alle Dateien vorhanden sind).
/* This thread displays a message on the screen.
  * This version uses "interrupt ()" and "Thread.interrupted ()"
  * ...
  * File: HelloOneThread.java            Author: S. Gross
  * Date: 17.09.2007
  */
public class HelloOneThread extends Thread
{
   private int threadID;

    /* constructors                                                                     */
    public HelloOneThread ()
    {
      this.threadID = 0;
      setName ("HelloThread-" + Integer.toString (threadID));
    }

    public HelloOneThread (int i)
    {
      this.threadID = i;
      setName ("HelloThread-" + Integer.toString (threadID));
    }

    /* implementation of method "run"                                                   */
    public void run ()
    {
      final int MAX_SLEEP_TIME = 2000;            /* max. 2 s                           */

        for (int i = 0; !Thread.interrupted (); ++i)
        {
          System.out.println (this.toString () + ": Hello!");
          try                                /* simulate work                           */
          {
            Thread.sleep ((int) (Math.random () * MAX_SLEEP_TIME));
          } catch (InterruptedException e)
          {
            System.err.println (getName () + ": OK, I will " +
                                 "terminate now.");
            /* The catch-statement has cleared the interrupt-status-flag
              * so that we must call "interrupt ()" once more (otherwise
              * the progam will continue to run).
              */
            interrupt ();
          }
        }
    }

    /* overloading "toString ()"                                                        */
    public String toString ()
    {
      return getName ();
    }
}

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                 Seite 4 - 9

In der zweiten Version dieses Programms stellt der „Thread“ selbst eine spezielle Methode zum
Stoppen des Threads zur Verfügung.
/* Program creates a thread which prints a message on the screen.
  * This version implements its own method "stopThread ()".
  * ...
  * File: HelloTwoMain.java               Author: S. Gross
  */
public class HelloTwoMain
{
   public static void main (String args[])
   {
     ...
     /* Terminate all threads.                                                         */
     for (int i = 0; i < NUMBER_OF_THREADS; ++i)
     {
        System.out.println ("HelloTwoMain: I stop thread '" +
                            hello[i].getName () + "' via " +
                            "'stopThread ()'.");
        (hello[i]).stopThread ();
     }
     ...
   }
}

/* This thread displays a message on the screen.
  * ...
  * File: HelloTwoThread.java             Author: S. Gross
  */
public class HelloTwoThread extends Thread
{
   private boolean isNotInterrupted;      /* interrupt received?                       */
   ...
   /* implementation of method "run"                                                   */
   public void run ()
   {
     ...
     isNotInterrupted = true;
     for (int i = 0; isNotInterrupted; ++i)
     {
        try                               /* simulate it                               */
        {
          Thread.sleep ((int) (Math.random () * MAX_SLEEP_TIME));
        } catch (InterruptedException e)
        {
          ...
          isNotInterrupted = false;       /* terminate loop                            */
        }
     }
   }

    /* allow termination of thread                                                     */
    public void stopThread ()
    {
      try
      {
        isNotInterrupted = false;
        this.interrupt ();
      } catch (SecurityException e) {...}
    }
}

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                   Seite 4 - 10

Die dritte Version des Programms benutzt eine Mischung aus den beiden ersten Versionen und
die vierte Version packt den gesamten Code der Methode „run ()“ in einen „try-catch“-Block.
Alternativ kann das Hauptprogramm auch mit „System.exit (0)“ die virtuelle Maschine von Java
und damit alle Threads beenden (wird z. B. später in „SyncBlock/SyncBlockOneMain.java“
benutzt). Mit den bisher erworbenen Kenntnissen kann das kleine Grafikprogramm um Threads
erweitert werden. Übersetzen und starten Sie das Programm MoveBallMain.java im Verzeichnis
MoveBallThreads. Die Oberfläche verhält sich jetzt so, wie sie sich verhalten soll, d. h., jeder
Klick wird sofort bearbeitet.

Manchmal ist es sinnvoll, Threads in Gruppen zusammenzufassen, damit eine Aktion für alle
Threads der Gruppe ausgeführt werden kann (z. B. beenden von Threads). Hierfür gibt es in Java
die Klasse ThreadGroup mit entsprechenden Methoden. Man kann das obige Programm zum
Beispiel um rote Bälle erweitern. Alle blauen Bälle gehören in eine Thread-Gruppe und alle
roten in eine andere. In diesem Fall könnte man alle Threads einer Gruppe auf einmal beenden.

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                       Seite 4 - 11

Ein Ball kann nur entfernt werden, wenn sein Thread noch aktiv ist, d. h., dass sich der Ball noch
bewegt. Bälle, die sich nicht mehr bewegen, gehören eigentlich nicht mehr auf die Zeichenfläche,
da es keinen Thread gibt, der sie bei einer Änderung der Fenstergröße neu zeichnet. Ich habe den
„letzten“ Ball nur auf der Zeichenfläche gelassen, damit man die letzte Aktion des Threads sehen
kann. Bei einer Größenänderung des Fensters würden diese Bälle automatisch gelöscht werden.
Der folgende Codeausschnitt zeigt das Erzeugen und Beenden der Threads (die Datei befindet
sich im Verzeichnis MoveBallTreadGroup).
/* ...
  * File: WindowWithButtons.java        Author: S. Gross
  */
...
public class WindowWithButtons extends JFrame
{
   ...
   private static int thrRedID = 0;     /* thread number                                     */
   private static int thrBlueID = 0;

    private ThreadGroup blueBalls = new ThreadGroup ("blue balls"),
                         redBalls = new ThreadGroup ("red balls");
    ...
    private class StartBlueBallButtonListener implements ActionListener
    {
      public void actionPerformed (ActionEvent event)
      {
        Thread ball =
          new Thread (blueBalls,
                       new MoveBall (canvas, Color.blue),
                       "MoveBlueBallThread-" +
                       Integer.toString (thrBlueID++));
        ball.start ();                     /* let the ball bounce around                     */
      }
    }

    private class RemoveBlueBallButtonListener implements ActionListener
    {
      public void actionPerformed (ActionEvent event)
      {
        blueBalls.interrupt ();
      }
    }
    ...
}

Jeder Thread hat eine Priorität. Manchmal ist es sinnvoll, die Priorität eines Threads zu ändern,
da nicht alle Java-Implementierungen ein Zeitscheibenverfahren (round-robin) unterstützen und
ein rechenintensiver Thread dadurch alle anderen Threads in ihrer Arbeit behindern kann (im
Extremfall arbeitet nur der eine Thread). Der Grund hierfür liegt darin, dass der Scheduler u. U.
nur dann aktiv werden kann, wenn ein Thread eine Thread-Methode aufruft (was ein rechen-
intensiver Thread u. U. nicht macht). Falls mehrere Threads eine gleich-hohe Priorität haben und
ein Zeitscheibenverfahren zur Verfügung steht, erhalten alle Threads zyklisch nacheinander je
eine Zeitscheibe. Falls die Threads verschiedene Prioritäten haben, erhält der Thread mit der
höchsten Priorität den Prozessor, sodass Threads mit geringer Priorität u. U. beliebig lange
warten müssen (starvation). Je höher die Priorität eines Threads ist, desto seltener sollte er in der
Regel den Prozessor belegen, damit alle Threads arbeiten können. Wenn viele Threads dieselbe
Priorität haben und ein Thread den Prozessor monopolisiert, kann man sich die Prioritäten aber

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                      Seite 4 - 12

auch zu Nutze machen, um das Scheduling einer Java-Implementierung ggf. zu „verbessern“,
indem man einen Thread hoher Priorität immer kurz aufweckt, um dem Scheduler die Gelegen-
heit zu geben, einen anderen Thread auf den Prozessor zu bringen. Die Klasse Thread stellt hier-
für die Konstanten „MAX_PRIORITY“, „MIN_PRIORITY“ und „NORM_PRIORITY“ sowie
die Methoden „getPriority ()“ und „setPriority ()“ zur Verfügung. Alle Threads, deren Priorität
nicht explizit mit „setPriority ()“ geändert worden ist, befinden sich in der Prioritätsstufe
NORM_PRIORITY. In der Regel werden unter Java die Prioritätsstufen eins bis zehn zur Verfü-
gung gestellt, wobei die Stufe fünf die Standardpriorität ist. Die virtuelle Java-Maschine kann die
Prioritätsstufen auf beliebige Weise implementieren. Sie kann die zehn Prioritätsstufen beispiels-
weise auf zehn oder weniger Prioritätsstufen des Betriebssystems abbilden oder die Werte auch
vollständig ignorieren. Unter Solaris werden die Prioritätsstufen in „Java 5“ z. B. auf fünf Priori-
tätsstufen des Betriebssystems abgebildet. Die Prioritätsstufen fünf bis zehn werden alle auf die
höchste Prioritätsstufe in der TS- oder IA-Klasse (timesharing, interactive) abgebildet und die
restlichen vier Prioritätsstufen auf entsprechende niedrigere Prioritätsstufen dieser Klassen
(http://docs.oracle.com/javase/1.5.0/docs/guide/vm/thread-priorities.html). Sie dürfen sich in
Ihren Programmen also nicht auf die Prioritätsstufen verlassen! Die Programme im Verzeichnis
ThreadScheduling demonstrieren die Arbeit mit Prioritäten.
/* This program demonstrates the scheduling of threads in Java. The
  * ...
  * File: ThreadSchedulingMain.java       Author: S. Gross
  */
public class ThreadSchedulingMain
{
   public static void main (String args[])
   {
     ...
     (new PrintThread (++thrID, TEXT_1)).start ();
     (new PrintThread (++thrID, TEXT_2)).start ();
     if ((cmd.compareTo ("prio") == 0))
     {
        HighPriorityThread prio = new HighPriorityThread (++thrID, SLEEP_TIME);
        prio.setPriority (Thread.MAX_PRIORITY);
        prio.start ();
     }
     ...
   }
}

/* This program demonstrates the scheduling of threads in Java.
  * ...
  * File: PrintThread.java              Author: S. Gross
  */
class PrintThread extends Thread
{
   private int    thrID;
   private String message;

  public PrintThread (int thrID, String message)
  {
    ...
  }

  public void run ()
  {
    String name = getName ();

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                    Seite 4 - 13

        /* decrease the priority so that the main thread is able to exit
          * even then when no round-robin scheduler is implemented.
          */
        this.setPriority (Thread.NORM_PRIORITY - 1);
        while (true)
        {
           for (int i = 0; i < 500000; ++i) /* simulate some work            */
           {
             Math.random ();
           }
           System.out.println (name + ": priority: " + this.getPriority () +
                               "    " + message);
        }
    }
}

/* This program demonstrates the scheduling of threads in Java.
  * ...
  * File: HighPriotityThread.java       Author: S. Gross
  */
class HighPriorityThread extends Thread
{
   private int thrID;
   private long sleeptime;
    public HighPriorityThread (int thrID, long sleeptime)
    { ...
    }
    public void run ()
    {
      String name = getName ();
        while (true)
        {
          /* The thread prints a message so that you can see that it was
            * invoked. Normally it won't print anything.
            */
          System.out.println (name + ": priority: " + this.getPriority ());
          try
          {
             sleep (sleeptime);
          } catch (InterruptedException e)
          {
          }
        }
    }
}

Die Klasse Thread stellt u. a. die folgenden Methoden zur Verfügung.
static int activeCount ()                aktuelle Anzahl Threads in der Gruppe des Threads
static Thread currentThread ()           liefert Referenz auf das aktuelle Thread-Objekt
static int enumerate (Thread[] tarray)   liefert Referenzen auf alle aktiven Thread-Objekte in
                                         der Gruppe und allen Untergruppen des aktuellen
                                         Threads; liefert die Anzahl der Einträge im Feld als
                                         Rückgabewert
long getID ()                            liefert den Identifikator des Threads (entspricht dem
                                         Identifikator beim Erzeugen des Threads)

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                    Seite 4 - 14

String getName ()                        liefert den Namen des Threads
int getPriority ()                       liefert die Priorität des Threads
ThreadGroup getThreadGroup ()            liefert die Thread-Gruppe, zu der der Thread gehört
void interrupt ()                        unterbricht den Thread
static boolean interrupted ()            testet, ob der Thread unterbrochen wurde („interrupt
                                         status flag“ wird gelöscht)
booelean isAlive ()                      testet, ob der Thread noch „lebt“
boolean isDaemon ()                      testet, ob Thread als Dämon läuft
boolean isInterrupted ()                 testet, ob der Thread unterbrochen wurde („interrupt
                                         status flag“ wird nicht verändert)
void join ()                             wartet auf das Ende des Threads
void join (long ms)                      wartet höchstens ms Millisekunden auf das Ende des
                                         Threads
void setDaemon (boolean on)              schaltet zwischen daemon thread und user thread um
                                         (muss aufgerufen werden, bevor der Thread gestartet
                                         wird)
void setName (String name)               ändert den Namen des Threads
void setPriority (int np)                setzt Priorität des Threads auf np
static void sleep (long ms)              Thread „schläft“ ms Millisekunden
void start ()                            Thread beginnt mit Ausführung
String toString ()                       liefert Beschreibung des Threads, im Allgemeinen
                                         (Name, Gruppe, Priorität, ...)
static void yield ()                     Thread verzichtet temporär auf Prozessor, sodass
                                         andere Threads ausgeführt werden können

Die Arbeit der Threads muss synchronisiert werden, wenn sie mit gemeinsamen Variablen arbei-
ten. Java bietet hierfür eine Synchronisation über Monitore, die sicherstellt, dass alle Anwei-
sungsblöcke und Methoden, die durch das Schlüsselwort „synchronized“ geschützt werden, zu
jedem Zeitpunkt von höchstens einem Thread benutzt werden können (der Zugriff wird sequen-
tialisiert). Falls ein Thread innerhalb einer solchen Methode auf eine Bedingung warten muss (z.
B. auf einen freien Speicherplatz oder einen Datensatz beim Erzeuger-/Verbraucherproblem),
kann er in der geschützten Methode die Methode „wait ()“ aus der Klasse Object von java.lang
aufrufen. Wenn er durch den Aufruf von „wait ()“, „wait (long timeout)“ oder „wait (long time-
out, int nanos)“ blockiert worden ist, werden automatisch alle Sperren, die er in diesem Monitor-
Objekt hatte, freigegeben und er nimmt am Scheduling nicht mehr teil. Er wird aus der Warte-
schlange befreit, wenn ein anderer Thread für das Monitor-Objekt „notify ()“ oder „notifyAll ()“
aufruft oder ein anderer Thread für ihn die Methode „interrupt ()“ aufruft oder die über den Para-
meter der wait-Methode eingestellte Zeit abgelaufen ist. In Java warten alle Threads in dersel-
ben Objekt-Warteschlange, d. h., dass ein aufgeweckter Thread erneut mit allen anderen Threads

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                       Seite 4 - 15

um den Zutritt zum Monitor-Objekt kämpfen muss, nachdem das Monitor-Objekt freigegeben
worden ist (ein Thread, der in einem Monitor-Objekt blockiert war, hat keine Priorität gegenüber
einem Thread, der den Monitor zum ersten Mal benutzen will). Wenn ein blockierter Thread
aufgeweckt worden ist, kann er deshalb nicht davon ausgehen, dass die Bedingung noch erfüllt
ist, wenn er den Monitor wieder besitzt. Sobald er den Monitor wieder betreten hat, werden alle
Sperren wieder in denselben Zustand versetzt, den sie zum Zeitpunkt des Aufrufs der Methode
„wait ()“ hatten. Falls der Thread durch „interrupt ()“ geweckt worden ist, wird die Ausnahme
InterruptedException nach Wiederherstellung seines Zustands im Monitor ausgelöst. Die Metho-
de „wait ()“ gibt nur die Sperren für das Objekt frei, in dem sie aufgerufen wurde, d. h., dass alle
anderen Objekte, die der Thread eventuell noch gesperrt hatte, gesperrt bleiben, während er in
der Warteschlange wartet. Durch Mehrfachsperren können also sehr leicht Verklemmungen
(deadlocks) entstehen. Die Methode „notify ()“ befreit einen beliebigen blockierten Thread aus
der Warteschlange (implementierungsabhängig), während „notifyAll ()“ alle blockierten Threads
aus der Objekt-Warteschlange befreit.
Falls Threads auf verschiedene Bedingungen warten, muss „notifyAll ()“ benutzt werden, da
sonst u. U. ein „falscher“ Thread geweckt wird, wie das folgende Beispiel zeigt. Betrachten wir
ein Erzeuger-/Verbraucherproblem mit zwei Erzeugern, einem Verbraucher, einem Puffer für
einen Datensatz sowie einer FIFO-Auswahlstrategie. Der Verbraucher kann zwei aufeinander-
folgende Pufferzugriffe machen, da ihm der Prozessor nicht entzogen wurde und er sich auch
nicht selbst blockiert hat, sodass P1 den Monitor noch nicht wieder betreten konnte.

 Aufgabe                          Puffer   wait-WS         Monitor-WS      Aktionen
 -                                leer     -               -               -
 P1 legt Datensatz ab             voll     -               -               notify ohne Wirkung
 P1 will Datensatz ablegen        voll     P1              -               P1 wird blockiert
 P0 will Datensatz ablegen        voll     P1, P0          -               P0 wird blockiert
 C0 liest Datensatz               leer     P0              P1              notify befreit P1
 C0 will Datensatz lesen          leer     P0, C0          P1              C0 wird blockiert
 P1 legt Datensatz ab             voll     C0              P0              notify befreit P0
 P0 will Datensatz ablegen        voll     C0, P0          -               P0 wird blockiert
 P1 will Datensatz ablegen        voll     C0, P0, P1      -               P1 wird blockiert
 ⇒ Blockade aller Threads

Die Methode notifyAll hätte die Blockade vermieden, da sie immer alle blockierten Threads
aufgeweckt hätte. Der Verbraucher hätte den Datensatz dann aus dem Puffer entnommen und
damit den Fortschritt der Erzeuger garantiert. Im Verzeichnis Threads/ProducerConsumer
befinden sich die entsprechend modifizierten Programme aus dem Programmarchiv der Haupt-
veranstaltung Betriebssysteme (zwei Erzeuger, ein Verbraucher, Puffer für einen Datensatz,
notify statt notifyAll, aktives Warten). Übersetzen und starten Sie das Programm und warten Sie
dann bis ggf. eine Blockade eintritt. Es hängt von der Java-Implementierung ab, ob es während
der Laufzeit des Programms tatsächlich zu einer Blockade kommt.

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                 Seite 4 - 16

Die Threads im Programm des Verzeichnisses SyncBlock erhöhen den Wert einer gemeinsamen
Variablen und geben den Wert der Variablen aus. Wenn der Zugriff auf die gemeinsame Variable
synchronisiert wird (SyncBlockOneMain.java), werden, wie erwartet, nur verschiedene Werte
ausgegeben. Falls der Zugriff nicht synchronisiert wird (SyncBlockTwoMain.java), werden einige
Werte mehrfach ausgegeben, da derselbe Wert von mehreren Threads gelesen, inkrementiert und
zurückgeschrieben wurde. Nachdem Sie das Programm übersetzt haben, können Sie es unter
UNIX mit dem folgenden Befehl ausführen, der nur mehrfach vorkommende Zahlen ausgibt
(„man awk“ und „man uniq“ liefern Ihnen Details zu den beiden UNIX-Befehlen).
java SyncBlockOneMain | awk '{ print $2 }' | sort | uniq -D bzw.
java SyncBlockTwoMain | awk '{ print $2 }' | sort | uniq -D

/* Monitors in Java: synchronizing a block within a method.
  * ...
  * File: SyncBlockOneMain.java          Author: S. Gross
  */
public class SyncBlockOneMain
{
   public static void main (String args[])
   {
     final long WAIT_TIME = 5000;        /* 5 seconds                                 */
     int        i         = 0;           /* thread ID                                 */

        SyncBlockOneThread thr1 = new SyncBlockOneThread (++i);
        SyncBlockOneThread thr2 = new SyncBlockOneThread (++i);
        SyncBlockOneThread thr3 = new SyncBlockOneThread (++i);
        thr1.start ();
        thr2.start ();
        thr3.start ();
        /* wait some time before terminating all threads                              */
        try
        {
          Thread.sleep (WAIT_TIME);
        } catch (InterruptedException e)
        {
        }
        System.exit (0);
    }
}

/* Monitors in Java: synchronizing a block within a method.
  * ...
  * File: SyncBlockOneThread.java       Author: S. Gross
  */
class SyncBlockOneThread extends Thread
{
   private int thrID;                   /* ID of this thread                          */
   static int cnt = 0;                  /* shared variable                            */

    public SyncBlockOneThread (int thrID)
    {
      this.thrID = thrID;
      setName ("SyncBlockOneThread-" + Integer.toString (thrID));
    }

    public void run ()
    {
      String name = getName ();

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                   Seite 4 - 17

        while (true)
        {
          synchronized (getClass ())
          {
            System.out.println (name + ": " + cnt);
            cnt = cnt + 1;                  /* long way to catch an error                */
          }
        }
    }
}

Der Synchronisationsfehler tritt auch dann auf, wenn Sie „cnt = cnt + 1“ durch „cnt++“ ersetzen.
Im Verzeichnis SyncBlockRunnable wurden die entsprechenden Programme über „implements
Runnable“ implementiert. Die Programme im Verzeichnis SyncMethod demonstrieren die
Synchronisation beim Zugriff auf Methoden. Die Methode „nextInt ()“ soll fortlaufende Zahlen
liefern. Wenn der Zugriff nicht synchronisiert wird, erhalten einige Threads dieselbe Zahl.
Starten Sie die Programme über die folgenden Befehle, damit Sie die Fehlfunktion ggf. einfach
sehen können.
java SyncMethodOneMain | awk '{ print $2 }' | sort | uniq -D bzw.
java SyncMethodTwoMain | awk '{ print $2 }' | sort | uniq -D
/* Monitors in Java: synchronizing a method.
  * ...
  * File: SyncMethodOneMain.java         Author: S. Gross
  */
public class SyncMethodOneMain
{
   public static void main (String args[])
   {
     final int           THR_NUM   = 4;          /* number of threads                    */
     final int           FIRST_VAL = 0;          /* start value                          */
     final long          WAIT_TIME = 5000;       /* 5 seconds                            */
     SyncMethodOneThread thr[]     = new SyncMethodOneThread[THR_NUM];
     SequentialIntOne    val       = new SequentialIntOne (FIRST_VAL);
        for (int i = 0; i < THR_NUM; ++i)
        {
          thr[i] = new SyncMethodOneThread (i, val);
          thr[i].start ();
        }
        /* wait some time before terminating all threads                                 */
        try
        {
          Thread.sleep (WAIT_TIME);
        } catch (InterruptedException e)
        {
        }
        System.exit (0);
    }
}

/* Monitors in Java: synchronizing a method.
  * ...
  * File: SyncMethodOneThread.java               Author: S. Gross
  */
class SyncMethodOneThread extends Thread
{
   private int              thrID;       /* ID of this thread                            */
   private SequentialIntOne val;

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                  Seite 4 - 18

    public SyncMethodOneThread (int thrID, SequentialIntOne val)
    {
      this.thrID = thrID;
      this.val   = val;
      setName ("SyncMethodOneThread-" + Integer.toString (thrID));
    }
    public void run ()
    {
      String name = getName ();
        while (true)
        {
          System.out.println (name + ": " + val.nextInt ());
        }
    }
}

/* Monitors in Java: synchronizing a method.
 * ...
 * File: SequentialIntOne.java          Author: S. Gross
 */
class SequentialIntOne
{
  int val;
    public SequentialIntOne (int val)
    {
      this.val = val;
    }
    public synchronized int nextInt ()
    {
      int oldInt;
        oldInt = val;
        for (int i = 0; i < 5000; ++i) /* simulate computation of new value */
        {
          Math.random ();
        }
        val++;
        return oldInt;
    }
}

Jeder Thread durchläuft in seinem Leben mehrere Zustände. Das folgende Diagramm zeigt die
Zustände eines Threads in der virtuellen Java-Maschine, wie sie von der Methode „getState ()“
geliefert werden.

                              new

                            runnable

         waiting   timed_waiting                    blocked

                                       terminated

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                    Seite 4 - 19

Der Thread befindet sich im Zustand „new“, wenn das Thread-Objekt erzeugt worden ist, aber
noch nicht gestartet wurde. Sobald der Thread gestartet worden ist, befindet er sich im Zustand
„runnable“, d. h., dass er in der virtuellen Maschine ausgeführt werden kann. Unter Umständen
muss er aber noch auf Betriebsmittel des Betriebssystems warten (z. B. die CPU). Wenn er z. B.
darauf wartet, einen Monitor zu betreten, befindet er sich im Zustand „blocked“. Er befindet sich
im Zustand „waiting“, wenn er beispielsweise „wait ()“ oder „join ()“ aufgerufen hat und im
Zustand „timed_waiting“, wenn er z. B. „wait ()“ oder „join ()“ mit einem Zeitparameter oder
„sleep ()“ aufgerufen hat. Wenn er seine Arbeit beendet hat, befindet er sich im Zustand
„terminated“. Die Thread-Zustände können seit Java 5 mit der Methode „getState ()“ abgefragt
werden. Übersetzen und starten Sie das Programm „ThreadStateMain.java“ im Verzeichnis
ThreadState.
/* Program shows the states of a thread in the "Java Virtual Machine".
 * ...
 * File: ThreadStateMain.java           Author: S. Gross
 */

public class ThreadStateMain
{
  public static void main (String args[])
  {
    final int LOOP_COUNT = 20;
    final int SLEEP_TIME = 1000;        /* 1 second                                       */

        int thrID = 0;                            /* thread ID                            */

        CountThread thr1 = new CountThread (++thrID);
        CountThread thr2 = new CountThread (++thrID);
        System.out.println (thr1.getName () + ": " + thr1.getState ());
        thr1.start ();
        thr2.start ();
        System.out.println (thr1.getName () + ": " + thr1.getState ());
        for (int i = 0; i < LOOP_COUNT; ++i)
        {
          System.out.println (thr1.getName () + ": " + thr1.getState ());
          try
          {
            Thread.sleep (SLEEP_TIME);
          } catch (InterruptedException e)
          {
          }
        }
    }
}

/* Program shows the states of a thread in the "Java Virtual Machine".
 * ...
 * File: CountThread.java               Author: S. Gross
 */

class CountThread extends Thread
{
  ...
  public void run ()
  {
    final int MAX_COUNT = 8;
    final int SLEEP_TIME = 1000;                  /* 1 second                             */

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                    Seite 4 - 20

        String name = getName ();

        for (int i = 0; i < MAX_COUNT; ++i)
        {
          try
          {
            Thread.sleep (SLEEP_TIME);
          } catch (InterruptedException e)
          {
          }
          synchronized (getClass ())
          {
            System.out.println (name + ": " + cnt);
            cnt++;
            try
            {
              Thread.sleep (SLEEP_TIME);
            } catch (InterruptedException e)
            {
            }
          }
        }
    }

    public String toString ()
    {
      return getName ();
    }
}

Neben den bisher besprochenen Threads (den sogenannten „user threads“) gibt es in Java noch
„daemon threads“. Diese Threads werden im Allgemeinen für Server eingesetzt, da sie automa-
tisch enden, wenn der letzte „user thread“ geendet hat. In Java endet ein Programm erst dann,
wenn der letzte Thread beendet worden ist. Falls ein Thread also in einer Endlosschleife läuft (z.
B. ein Server-Thread), muss er beispielsweise mit „interrupt ()“ beendet werden oder das Haupt-
programm kann „System.exit (0)“ aufrufen und damit die virtuelle Maschine mit allen Threads
beenden. Eine weitere Möglichkeit besteht darin, solche Threads als „daemon threads“ zu star-
ten. Die Methode „setDaemon ()“ muss aufgerufen werden, bevor der Thread gestartet wird. Den
Unterschied zwischen „user threads“ und „daemon threads“ zeigen die Programme im Ver-
zeichnis ThreadDaemon. Übersetzen und starten Sie die beiden Hauptprogramme.

/* Program shows the behaviour of daemon threads compared to
 * user threads.
 * ...
 * File: ThreadDaemonTrueMain.java      Author: S. Gross
 */

public class ThreadDaemonTrueMain
{
  public static void main (String args[])
  {
    final int WAIT_TIME = 2000;         /* 2 seconds                                      */
    DaemonUserThread thr = new DaemonUserThread ();
    thr.setDaemon (true);
    thr.start ();
    System.out.println ("I've started a daemon thread");

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                             Seite 4 - 21

        try
        {
          Thread.sleep (WAIT_TIME);
        } catch (InterruptedException e) { }
        System.out.println ("Nothing more todo.");
    }
}

/* Program shows the behaviour of daemon threads compared to
  * user threads.
  * ...
  * File: DaemonUserThread.java         Author: S. Gross
  */
class DaemonUserThread extends Thread
{
   public void run ()
   {
     final int SLEEP_TIME = 1000;       /* 1 second                               */

        while (true)
        {
          if (isDaemon ())
          {
            System.out.println (getName () + ": Running as daemon thread");
          }
          else
          {
            System.out.println (getName () + ": Running as user thread");
          }
          try
          {
            Thread.sleep (SLEEP_TIME);
          } catch (InterruptedException e) { }
        }
    }
}

Wenn Sie das Programm ThreadDaemonFalseMain starten, läuft der Thread beliebig lange
weiter und Sie müssen das Programm z. B. mit  zwangsweise beenden.
eiger ThreadDaemon 149 java ThreadDaemonFalseMain
I've started a user thread
DaemonUserThread: Running as user thread
DaemonUserThread: Running as user thread
Nothing more todo.
DaemonUserThread: Running as user thread
...

Wenn Sie das Programm ThreadDaemonTrueMain starten, läuft der Thread nur solange wie das
Hauptprogramm.
eiger ThreadDaemon 150 java ThreadDaemonTrueMain
I've started a daemon thread
DaemonUserThread: Running as daemon thread
DaemonUserThread: Running as daemon thread
Nothing more todo.
eiger ThreadDaemon 151

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                   Seite 4 - 22

Zum Schluss wollen wir uns noch einmal mit dem kleinen Grafikprogramm vom Anfang des
Kapitels beschäftigen. Dieses Programm hat zwar anschaulich gezeigt, dass Threads sinnvoll
sind, ist aber gleichzeitig eine vollkommen falsche und sogar gefährliche Lösung für unsere
Animation, da die Swing-Routinen nicht „thread-safe“ sind. Das Hauptprogramm der Animation
sorgt dafür, dass ein Fenster mit den entsprechenden Knöpfen (Buttons) erzeugt wird. Nachdem
das Fenster mit allen Komponenten erzeugt worden ist, wird es sichtbar gemacht (z. B. mit der
Methode „setVisible (true)“. In diesem Moment wird von Java automatisch ein Thread zur Ereig-
nisverwaltung (der sogenannte „Event Dispatch Thread“) gestartet. Nur dieser Thread darf von
jetzt an Änderungen am Fensterinhalt vornehmen. Der „Event Dispatch Thread“ wird als norma-
ler „user thread“ gestartet, sodass das Hauptprogramm z. B. explizit „System.exit (0)“ aufrufen
sollte, wenn es beendet werden soll. Das Programm vom Anfang dieses Kapitels meldet in dem
Fenster, in dem es gestartet wurde, was gerade passiert. Wie Sie der Ausgabe entnehmen können,
bewegt der Thread zur Ereignisverwaltung mit dem Namen „AWT-EventQueue-0“ die Bälle. Da
der Thread mit dem Ball beschäftigt ist, kann er natürlich nicht sofort auf Eingaben im Fenster
reagieren.
eiger MoveBall 6 javac MoveBallMain.java
eiger MoveBall 7 java MoveBallMain
AWT-EventQueue-0: new ball; color: blue; position: (47,322); step size: (2,6)
AWT-EventQueue-0: new ball; color: blue; position: (495,26); step size: (6,6)
eiger MoveBall 8

In den Thread-Programmen wird der Ball von MoveBall-Threads bewegt und der Thread zur
Ereignisverwaltung kann sich seiner eigentlichen Aufgabe widmen und auf Ereignisse im Fenster
reagieren. Da die MoveBall-Threads ihre Änderungen direkt in der Zeichenfläche des Fensters
vornehmen, entsprechen sie nicht der Philosophie und Architektur von Swing-Programmen. In
dem kleinen Beispiel ist es zwar nie zu schwerwiegenden Fehlern gekommen, aber laut Doku-
mentation können derartige Fehler auftreten. Wenn Sie den Ball vergrößern, erkennen Sie weite-
re Schwächen des Programms (im Verzeichnis MoveBigBallThreadGroup).

Da die Bälle direkt in der Zeichenfläche erstellt werden, flimmert das Bild. Eine flimmerfreie
Lösung benötigt eine Zwischenpufferung der Zeichnung (sogenanntes „double buffering“). Die

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                      Seite 4 - 23

Bälle werden mit der XOR-Funktion gezeichnet, damit das Programm einfach implementiert
werden konnte. Wie Sie der obigen Grafik entnehmen können, hat das zur Folge, dass neue Ball-
farben entstehen oder Teile eines Balls in der Hintergrundfarbe gezeichnet werden, wenn sich
Bälle überlappen. Wenn Sie viele Bälle starten, wird die Anwendung manchmal sehr langsam
und der „Event Dispatch Thread“ reagiert wieder sehr schlecht auf Änderungswünsche (z. B.
Fenstergröße ändern oder „Close“ klicken). Wenn Sie in der Methode „run ()“ der Datei
„MoveBall.java“ die Konstante „SLEEP_TIME“ auf z. B. 5000 (fünf Sekunden) erhöhen, einen
Ball starten und dann die Fenstergröße ändern, passiert erst einmal nichts. Wenn die Zeit abge-
laufen ist, wird der Ball erneut gezeichnet. Normalerweise sollte der Fensterinhalt sofort aktuali-
siert werden. Wenn sich die Größe des Fensters ändert oder ein Fenster bzw. ein Teil eines Fens-
ters aus dem Hintergrund in den Vordergrund bewegt wird, ruft der „Event Dispatch Thread“
automatisch die Methode „paint ()“ auf. Es bietet sich deshalb an, diese Methode zu überschrei-
ben und alle Änderungen des Fensterinhalts nur in dieser Methode vorzunehmen. Wenn ein Teil
der Grafik neu erstellt worden ist, kann z. B. die Methode „repaint ()“ aufgerufen werden und
damit ein Neuzeichnen des Fensterinhalts erzwungen werden. Da das Fenster verschiedene
Objekte enthalten kann, ist es auch sinnvoll, dass sich die Objekte selbst in einen Zwischenpuffer
zeichnen, der dann später in das Fenster kopiert wird. Die Objekte könnten beispielsweise in
einer Liste verwaltet werden. Falls die Grafik sehr kompliziert und damit rechenintensiv ist, ist es
im Allgemeinen auch sinnvoll, dass nicht der gesamte Fensterinhalt neu gezeichnet wird, sondern
nur der Teil, der geändert oder aus dem Hintergrund in den Vordergrund geholt wurde. Bei dieser
einfachen Grafik können wir auf zusätzliche Threads vollständig verzichten, da die Animation
auch über eine Uhr (Timer) realisiert werden kann. Im Verzeichnis MoveBallFinalVersion fin-
den Sie eine bessere Lösung für die Animation. Der Ball wird jetzt „nur noch“ als Objekt
implementiert.
/* Let a ball bounce around within a canvas. The coordinate (0, 0)
 * is in the upper left corner of the window.
 * ...
 * File: BallTwo.java                   Author: S. Gross
 */

import javax.swing.*;                                 /* needed for Swing classes           */
import java.awt.*;                                    /* needed for Colors                  */

public class BallTwo
{
  private static final       int   XSIZE          =    80;      /* size of ball             */
  private static final       int   YSIZE          =    80;
  private static final       int   STEP_SIZE_X    =    6;       /* max. step size for       */
  private static final       int   STEP_SIZE_Y    =    6;       /*   new ball position      */

  private   JPanel       canvas;                      /*   drawing area                     */
  private   Color        color;                       /*   color of ball                    */
  private   Graphics     g;                           /*   current graphics context         */
  private   Dimension    d;                           /*   size of canvas                   */
  private   int          x, y;                        /*   current position of ball         */
  private   int          dx, dy;                      /*   increments to move the ball      */
  private   int          ballNum;                     /*   ID of ball                       */

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                Seite 4 - 24

    public BallTwo (JPanel canvas, Color color, int id)
    {
      this.canvas = canvas;
      this.color   = color;
      this.ballNum = id;
      this.g       = canvas.getGraphics ();
      this.d       = canvas.getSize ();
      /* compute random speed of ball. "+ 1" avoids balls running on
       * horizontal or vertical lines
       */
      this.dx     = (int) (Math.random () * STEP_SIZE_X) + 1;
      this.dy     = (int) (Math.random () * STEP_SIZE_Y) + 1;
      /* draw first ball at random starting point                                     */
      this.x      = (int) (Math.random () * d.width);
      this.y      = (int) (Math.random () * d.height);
    }

    public BallTwo (JPanel canvas, Color color, int id, int x, int y)
    {
      this.canvas = canvas;
      this.color   = color;
      this.ballNum = id;
      this.x       = x;
      this.y       = y;
      this.g       = canvas.getGraphics ();
      this.d       = canvas.getSize ();
      /* compute random speed of ball. "+ 1" avoids balls running on
       * horizontal or vertical lines
       */
      this.dx = (int) (Math.random () * STEP_SIZE_X) + 1;
      this.dy = (int) (Math.random () * STEP_SIZE_Y) + 1;
    }

    public void paintBall (Graphics g)
    {
      /* check if ball is inside the canvas (possibly the size
       * of the canvas has changed) -> adjust position if necessary
       * so that ball is still inside the canvas
       */
      checkUpdateBallPosition ();
      g.setColor (color);
      g.fillOval (x, y, XSIZE, YSIZE);
    }

    /* compute new ball position                                                      */
    public void newBallPosition ()
    {
      x += dx;                                    /* new ball position                */
      y += dy;
    }
    ...
}

Falls die Methode „paintBall ()“ umfangreiche Berechnungen durchführen müsste, könnte man
diese Arbeiten auch in einem separaten Thread durchführen lassen. Die Methode „paintBall ()“
wird in der Methode „paint ()“ der Klasse „MyJPanelTwo“ aufgerufen, wie Sie dem folgenden
Programm entnehmen können. In dieser Klasse wird auch ein Uhren-Thread gestartet, der dafür
sorgt, dass der Fensterinhalt regelmäßig aktualisiert wird.

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                             Seite 4 - 25

/* Draw some objects.
  * ...
  * File: MyJPanelTwo.java               Author: S. Gross
  */
...
public class MyJPanelTwo extends JPanel
{
   static final long serialVersionUID = 1126359778513707255L;
    private ArrayList ballsList          = new ArrayList ();
    public MyJPanelTwo ()
    {
      final int DELAY = 5;
        /* use a timer thread to update the screen                                */
        PaintBallsListener paintBalls = new PaintBallsListener ();
        Timer              t          = new Timer (DELAY, paintBalls);
        t.start ();
    }
    public void paint (Graphics g)
    {
      super.paint (g);
      for (BallTwo b: ballsList)
      {
        b.paintBall (g);
      }
    }
    public void addBall (BallTwo b)
    {
      ballsList.add (b);
      System.out.println ("New " + b.toString ());
      repaint ();
    }
    public void removeBalls (String color)
    {
      BallTwo b;
      Iterator it = ballsList.iterator ();

        while (it.hasNext ())
        {
          b = it.next ();
          if (b.getBallColor() == color)
          {
            it.remove();
          }
        }
        repaint ();
    }
    private class PaintBallsListener implements ActionListener
    {
      public void actionPerformed (ActionEvent event)
      {
        for (BallTwo b: ballsList)
        {
          b.newBallPosition ();
        }
        repaint ();
      }
    }
}

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme                                Seite 4 - 26

Der folgenden Datei können Sie entnehmen, wie das Fenster mit der Zeichenfläche und den ver-
schiedenen Routinen zur Behandlung von Ereignissen erzeugt wird.
/* Create a window with a drawing area (canvas) at the top of the
 * windows and the following buttons at the bottom.
 *   Start blue ball    start a blue ball moving within the window
 *   Start red ball     start a red ball moving within the window
 *   Remove blue balls remove all blue balls
 *   Remove red balls   remove all red balls
 *   Close              close the window
 * This version installs a MouseListener for the drawing area. A
 * click with any button creates a "green ball". A "double click"
 * removes all green balls.
 * ...
 * File: WindowWithButtonsTwo.java       Author: S. Gross
 */

import javax.swing.*;                             /* needed for Swing classes         */
import java.awt.*;                                /* needed for Container             */
import java.awt.event.*;                          /* needed for ActionListener        */

public class WindowWithButtonsTwo extends JFrame
  implements MouseListener
{
  static final long serialVersionUID = -4612637987030198879L;

  private MyJPanelTwo canvas;                     /* drawing area                     */
  private int         ballNum;                    /* ID of ball                       */

  public WindowWithButtonsTwo ()
  {
    final String DEFAULT_TITLE                      =   "Bouncing Ball";
    final int    DEFAULT_WINDOW_WIDTH               =   640;
    final int    DEFAULT_WINDOW_HEIGHT              =   480;
    final int    UPPER_LEFT_WINDOW_EDGE_X           =   100;
    final int    UPPER_LEFT_WINDOW_EDGE_Y           =   40;

      ballNum = 1;
      setTitle (DEFAULT_TITLE);
      setSize (DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT);
      setLocation (UPPER_LEFT_WINDOW_EDGE_X, UPPER_LEFT_WINDOW_EDGE_Y);
      /* specify what happens when the window will be closed                          */
      setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
      /* build container with canvas and panel with buttons                           */
      buildContainer ();
      setVisible (true);                  /* display the window                       */
  }

  /* The window will be created with the BorderLayout. Since "North",
    * "East", and "West" are unused, "Center" will use the whole top
    * of the window. The buttons are in "South".
    */
  private void buildContainer ()
  {
     Container canvasAndButtonPanel;
     JPanel    buttonPanel;
     JButton   startBlueBallButton, startRedBallButton,
               removeBlueBallButton, removeRedBallButton, closeButton;

      canvasAndButtonPanel = getContentPane ();
      /* create and add canvas                                                        */

Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Sie können auch lesen