Java Programmierung

Kap.12:  FÄDEN (Thread) DES ABLAUFS (Runnable)              


Kapitel-Index
  12.1   Einfache Vorrangsteuerung beim Multithreading  
  12.1.1    Thread-Vorrangsteuerung mit sleep(ms)  
  12.1.2    Thread-Vorrangsteuerung mit setPriority(prio)
  12.2   Sychronisiertes (synchronized) Multithreading  
  12.2.1    Synchronisierter Zugriff auf gemeinsames Objekt 
  12.2.2    Synchronisierter Aufruf gemeinsamer Methode 
  12.3   Testfragen
  Eine Folge von Programmschritten, einer nach dem anderen abgearbeitet, nennt man in Java 'thread' (Faden), was etwa dem Begriff 'task' in Assemblersprachen oder 'process' in anderen Programmiersprachen entspricht. Die bisherigen Kapitel behandelten nur Programme mit einer einzelnen Folge von Programmschritten, d.h. singlethreading (single tasking, serial processes).

  In diesem Kapitel wird die gleichzeitige Ausführung mehrerer Folgen von Programmschritten behandelt, d.h. multithreading (multitasking, parallel processes). Multithreading kann intern verwirklicht werden per Hardware, d.h. Einsatz von verschiedenen Prozessoren für threads, oder per Software, d.h. Aufteilung der einzelnen thread-Laufzeiten in Zeitscheiben.

  Dem Programmierer bietet Java im Standardpaket java.lang die
                        Schnittstelle Runnable

für die Implementierung des run() Laufs von threads und die
                        Klasse Thread

u.a. für die Vorgabe sleep(long), der nicht durch Wecken abkürzbaren Ruhezeiten der threads, das Setzen von Prioritäten setPriority(int) und das Warten join() eines thread, z.B. main(String[]), auf das Auslaufen eines anderen thread.


  In den Beispielen dieses Kapitels werden threads vereinbart mit Thread-Konstruktoren der Form

                 public Thread(Runnable target) {...}

wobei target ein Zielobjekt ist, dessen Klasse die Schnittstelle Runnable implementieren muss (implements Runnable) mit einer Methode run(), die beim start() des threads aufgerufen wird.


12.1     Einfache Vorrangsteuerung beim Multithreading

12.1.1   Thread-Vorrangsteuerung mit sleep(ms)

  Vorrangsteuerung beim multithreading ist möglich durch Angabe von Ruhezeiten für threads mit sleep(ms) in Millisekunden ms,
 z.B. (siehe WindGarantie unten)
       try {for(;;){sleep(ms);out.print(wetter);}}
       catch(InterruptedException e) {err.println(e.toString());}
  Die Ausnahme InterruptedException, die sleep() wirft, falls ein anderer Prozess (thread) diesen Prozess ausnahmsweise unterbricht, muss abgefangen werden.

  Im folgenden Beispiel WindGarantie schläft "wind" weniger als "calm".
//********************** WindGarantie.java ***********************
//                  Garantiert Wind beim Segeln.                 *
//     Two infinite threads in multithreading, with sleep().     *
//****************************************************************
//            java.lang.                                  Object
import static java.lang.System.*;                       // `System 

class Garantie extends Thread               // implements Runnable   
 {private String      wetter;
  private long        ms;
  Garantie(String wetter,long ms)
                     {this.wetter=wetter;this.ms=ms;}
  public void run()
   {try
     {for(;;)        {sleep(ms);out.print(wetter);}}
    catch(InterruptedException e) {err.println(e.toString());}
   }
 }

class WindGarantie                          
 {public static void main(String[] args)         // args ungenutzt
   {out.println      ("Segeln Start");
    String[]          wetter={"calm ","wind "};
    int[]             ms={300,100};
    Garantie[]        thread=new Garantie[wetter.length];
    for(int           i=0;i<wetter.length;i++)
     {(thread[i]=     new Garantie(wetter[i],ms[i])).start();}
    for(int           i=0;i<wetter.length;i++)
     try
      {thread[i].join();}            // main() waits for thread[i]
     catch(InterruptedException e) {err.println(e.toString());}
    out.println      ("Segeln Ende"); // vorher Abbruch mit Ctrl c
   }
 }
//****************************************************************

| Output, abgebrochen mit Ctrl(c)
+----------------------------------------------------------------
|Segeln Start
|wind calm calm calm wind wind wind wind wind calm wind wind calm
|wind calm calm wind wind wind calm wind calm wind calm wind wind
|wind calm calm calm wind wind wind calm calm wind wind calm calm
12.1.2   Thread-Vorrangsteuerung mit setPriority(prio)

  Vorrangsteuerung beim multithreading ist auch möglich durch Setzen von Prioritäten für threads mit setPriority(prio). Die Prioritäten sind nicht absolut zwingend, sondern nur Durchschnittswerte, so dass auch threads minderer Priorität ihren gebührenden Anteil am multithreading erhalten. Allerdings gibt es in Java nur 10 Prioritäten 1=Thread.MIN_PRIORITY ... Thread.MAX_PRIORITY=10 , darunter die mittlere Priorität Thread.NORM_PRIORITY=5, so dass Feinsteuerung des Vorrangs schwer fällt.

  Im folgenden Beispiel WindVorrang hat "wind" Vorrang vor "calm".
//*********************** WindVorrang.java ***********************
//                  Vorrangig Wind beim Segeln.                  *
//  Two infinite threads in multithreading, with setPriority().  *
//****************************************************************
//            java.lang.                                Object
import static java.lang.System .*;                    // ||`System
import static java.lang.Integer.MAX_VALUE;            // |`Integer
import static java.lang.Thread .MIN_PRIORITY;         // `Thread

class Vorrang extends Thread                // implements Runnable   
 {private String      wetter;
  Vorrang(String wetter,int prio)
                     {this.wetter=wetter;this.setPriority(prio);}
  public void forerun()
   {for(int           i=0;i<MAX_VALUE/300;i++){}}
  public void run()
   {for(;;)          {forerun();out.print(wetter);}}
 }
class WindVorrang                          
 {public static void main(String[] args)         // args ungenutzt
   {out.println      ("Segeln Start");
    String[]          wetter={"calm ","wind "};
    int[]             prio={MIN_PRIORITY,MIN_PRIORITY+1};
    Vorrang[]         thread=new Vorrang[wetter.length];
    for(int           i=0;i<wetter.length;i++)
     {(thread[i]=     new Vorrang(wetter[i],prio[i])).start();}
    for(int           i=0;i<wetter.length;i++)
     try
      {thread[i].join();}            // main() waits for thread[i] 
     catch(InterruptedException e) {err.println(e.toString());}
    out.println      ("Segeln Ende"); // vorher Abbruch mit Ctrl c
   }
 }
//****************************************************************
| Output, abgebrochen mit Ctrl(c)
+----------------------------------------------------------------
|Segeln Start
|wind calm calm calm wind wind wind wind wind calm wind wind calm
|wind calm calm wind wind wind calm wind calm wind calm wind wind
|wind calm calm calm wind wind wind calm calm wind wind calm calm
  Multithreading von threads, die nur aus der Wiederholung einer einzelnen kurzen Druck-Anweisung bestehen, lässt sich schwer mit Vorrang steuern. Daher wurde der Lauf run() der threads durch einen Vorlauf forerun() ergänzt, der die Laufzeit verlängert, ohne die Druck-Anweisung zu verändern.

  Die Beispiele WindGarantie (12.1.1) und WindVorrang (12.1.2) rerepräsentieren die "heile Welt" des multithreadings, denn es wird von den threads nicht auf gemeinsame Ressourcen, z.B. eine gemeinmeinsame Variable (shared variable, 12.2), zugegriffen, ausgenommen der gemeinsame Zugriff mit out.print() auf den Ausgabe-Monitor. Aber der schnelle und kompakte Druck
                       out.print(wetter);
in diesen Beispielen gibt dem multithreading offenbar keine Gelegenheit, durch verzahnte Ausgabe "Drucksalat" (12.2.1) oder "illegalen Zustand" (12.2.2) zu erzeugen.


12.2     Synchronisiertes (synchronized) Multithreading


  In den folgenden Beispielen Tilgungen (12.2.1) und Trauungen (12.2.2) wird von mehreren threads auf eine gemeinsame Variable (shared variable) zugegriffen.


  Für die Vermeidung von Zugriffsüberschneidungen, d.h. zwischen dem Lesen des alten Werts der gemeinsamen Variablen und dem davon abhängigen Schreiben eines neuen Werts durch einen thread könnte ein anderer thread bereits den alten Wert verändert haben, bietet Java im

            Sprachumfang 'synchronized multithreading'

in Form der synchronized-Anweisung (siehe Syntax.069, 12.2.1) und des Methoden-Modifizierers synchronized (siehe Syntax.070, 12.2.2) sowie in der

                        Klasse Object

u.a. die Methoden wait() für Versetzung von threads in den Warte- zustand und die Methoden notify(), notifyAll() für das Wecken der wartenden threads.
  Als Alternative zum 'synchronized multithreading' gibt es seit der Version Java 1.5 das
                 Paket java.util.concurrent
u.a. mit der generischen Schnittstelle BlockingQueue<E> und den Klassen Semaphore und ThreadPoolExecutor sowie das
                 Paket java.util.concurrent.locks
u.a. mit der Schnittstelle Lock und der Klasse ReentrantLock, die in diesem Buch aus Platzgründen nicht ausführlich behandelt werden können.

  Vorteile des 'synchronized multithreading' sind die Eleganz, Flexibilität und Schlankheit des Konzepts. Vorteile der concurrent -Pakete sind die bereitgestellten umfangreichen Schnittstellen und Klassen, z.B. für Pool-Modelle von Threads und Semaphore oder z.B. für Erzeuger-Verbraucher-Anwendungen, die der Benutzer sonst selbst implementieren müsste. Java verstößt mit zwei alternativen Konzepten für "parallele Prozesse" gegen das Prinzip des 'orthogonal design' (van Wijngaarden, ALGOL 68) für Programmiersprachen.




12.2.1   Synchronisierter Zugriff auf gemeinsames Objekt

  Thread-Kommunikation in einem Block mit Zugriff auf ein gemeinsames Objekt kann mit einer synchronized-Anweisung (Synchronized -Statement) programmiert werden, die nach Syntaxdiagramm 069 (für Statement, hier ein Auszug 069°) von der Form ist
069: SynchronizedStatement        SynchronizedAnweisung
    HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
    H                                                         H                        
    H                           non-null                      H
    H                         ReferenceType                   H
    H                        ,------------,       ,-------,   H                  
    H -> synchronized -> ( ->| Expression |-> ) ->| Block |-> H                         
    H                        '------------'       '-------'   H
    H                 locked/unlocked for others              H
    H                                                         H                        
    H                                                         H                        
    HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH

                z.B. (siehe unten Tilgungen)
                      synchronized(this) {...}
  Das folgende Programm Tilgungen hat fünf threads vom Typ Schuld, die synchronisiert zugreifen auf ein Objekt KONTO vom Typ Konto mit einer gemeinsamen Variablen (shared variable) stand.

  Multithreading von threads, die nur aus einer einzelnen kurzen Druck-Anweisung bestehen, könnte die nichtdeterminierte Reihenfolge der Abarbeitung von threads im multithreading reduzieren auf die determinierte Reihenfolge der thread-Vereinbarungen. Daher wurde der Lauf run() der threads durch einen Vorlauf forerun() ergänzt, der die Laufzeit verlängert, ohne die Druck-Anweisung zu verändern.
//************************ Tilgungen.java ************************
// Fuenf konkurrierende Schulden werden aus einem KONTO getilgt. *
//     finite threads,  shared variable, synchronized block.     *
//    Nicht-synchronisierte Version erzeugt Fehler/Drucksalat.   *
//****************************************************************
//            java.lang.                                 Object
import static java.lang.System .*;                     // |`System
import static java.lang.Integer.MAX_VALUE;             // `Integer
class Konto
 {private int           stand=10000;            // shared variable
  public int gibStand()
   {return              stand;}
  public void tilg(int betrag)
   {
    synchronized(this)         // ohne diese Zeile nicht synchron!
     {out.printf       ("%5d EUR Schuld ",betrag);
      if               (betrag<=stand)
           {out.println("getilgt");stand-=betrag;}
      else {out.println("besteht");}
 } } }                                           // Block Freigabe
class Schuld extends Thread  // implements Runnable, wrappes Konto
 {private final Konto   konto;
  private final int     betrag;
  public Schuld(Konto konto,int betrag)
                       {this.konto=konto;this.betrag=betrag;}
  public void forerun(){for(int i=0;i<MAX_VALUE/300;i++){}}
  public void run()    {forerun();konto.tilg(betrag);}
 }
class Tilgungen
 {public static void main(String[] args)         // args ungenutzt
   {final Konto         KONTO=new Konto();
    out.printf         ("%5d EUR KONTO\r\n",KONTO.gibStand());
    int[]               betrag={1000,2000,3000,4000,5000};
    Schuld[]            thread=new Schuld[betrag.length];
    for(int             i=0;i<betrag.length;i++)
     {(thread[i]=new Schuld(KONTO,betrag[i])).start();}
    for(int             i=0;i<betrag.length;i++) 
     try {thread[i].join();}  // main() waits for thread[i] to die
     catch(InterruptedException e) {err.println(e.toString());}
    out.printf         ("%5d EUR KONTO\r\n",KONTO.gibStand());
 } }                                                               
//****************************************************************

| Output (zufaellig)       | nicht-synchron Output (zufaellig)
+-----------------------   +----------------------------------
|10000 EUR KONTO           |10000 EUR KONTO     
| 3000 EUR Schuld getilgt  | 3000 EUR Schuld getilgt
| 5000 EUR Schuld getilgt  | 5000 EUR Schuld getilgt
| 4000 EUR Schuld besteht  | 4000 EUR Schuld getilgt
| 1000 EUR Schuld getilgt  | 1000 EUR Schuld getilgt
| 2000 EUR Schuld besteht  | 2000 EUR Schuld getilgt
| 1000 EUR KONTO           |    0 EUR KONTO
12.2.2   Synchronisierter Aufruf gemeinsamer Methode  

  Thread-Kommunikation mit Warten wait() von threads, Wecken notifyAll() aller wartenden threads und Zugriff auf eine gemeinsame Variable (shared variable) kann mit einer als 'synchronized' modifizierten gemeinsamen Methode programmiert werden.
//************************ Trauungen.java ************************
//  Zehn konkurrierende Kandidaten werden in einem AMT getraut.  *
//      finite threads, shared variable, synchronized method.    *
//  Nicht-synchronisierte Version wirft Ausnahme "..Illegal..".  *
//****************************************************************
//            java.lang.                                 Object 
import static java.lang.System .*;                     // |`System
import static java.lang.Integer.MAX_VALUE;             // `Integer
class Amt
 {private boolean       isBraut=false;          // shared variable 
  synchronized                 // ohne diese Zeile nicht synchron!
   public void trau(String name,boolean isBraut)
    {while             (this.isBraut==isBraut)
      try              {wait();}        // Methode trau() Freigabe
      catch(InterruptedException e) {err.println(e.toString());}
                        this.isBraut=isBraut;
     out.print         (name+(isBraut ?  " & " : "\r\n"));
     notifyAll         ();             // Wecken wartender threads
 }  }                                   // Methode trau() Freigabe 
class Kandidat extends Thread  // implements Runnable, wrappes Amt
 {private final Amt     amt;
  private final String  name;
  private final boolean isBraut;
  public Kandidat(Amt amt,String name,boolean isBraut)
              {this.amt=amt;this.name=name;this.isBraut=isBraut;}
  public void forerun(){for(int i=0;i<MAX_VALUE/300;i++){}}
  public void run()    {forerun();amt.trau(name,isBraut);} 
 }
class Trauungen
 {public static void main(String[] args)         // args ungenutzt
   {final Amt           AMT=new Amt();
    out.println        ("AMT oeffnet");
    String[]      name={"Carla","Emily","Gerda","Henny","Paula",
                        "Carl" ,"Emil" ,"Gerd" ,"Hein" ,"Paul"};
    boolean[]  isBraut={true   ,true   ,true   ,true   ,true  ,
                        false  ,false  ,false  ,false  ,false };
    Kandidat[]          thread=new Kandidat[name.length];
    for(int             i=0;i<name.length;i++)
     {(thread[i]=new Kandidat(AMT,name[i],isBraut[i])).start();}
    for(int             i=0;i<name.length;i++) 
     try {thread[i].join();}  // main() waits for thread[i] to die
     catch(InterruptedException e) {err.println(e.toString());}
    out.println        ("AMT schliesst");
 } }                                                               
//****************************************************************

| Output         | nicht-synchron Output
+-------------   +----------------------------------
|AMT oeffnet     |...IllegalMonitorStateException... 
|Carla & Hein
|Paula & Gerd
|Henny & Emil
|Emily & Carl
|Gerda & Paul
|AMT schliesst

  Im obigen Beispiel Trauungen verhindert die synchronisierte Methode trau(String,boolean) im Objekt AMT der Klasse Amt

 (siehe oben Trauungen)
  synchronized public void trau(String name,boolean isBraut){...}

die Trauung zweier Kandidaten, die beide Bräute (isBraut==true) oder beide keine Bräute sind, durch Vergleich mit der gemeinsamen Variablen (shared variable) this.isBraut von Amt. Der Block der synchronisierten Methode trau(String,boolean) entspricht dem Standard-Muster
         {while ... try {wait();} catch ... notifyAll();}

und beschreibt den Zusammenhang von wait() und notifyAll(). Der Aufruf von trau(String,boolean) durch ein aktives thread bewirkt die Sperrung von trau für andere threads und, falls diese trau aufrufen, deren Versetzung in den Wartezustand. Der Aufruf von wait() bewirkt die Versetzung des aktiven threads in den Wartezustand und die sofortige Freigabe von trau für andere threads.

  Es kann nur jeweils ein thread zur Zeit die gemeinsame Methode trau(String,boolean) durchlaufen. Die anderen threads müssen in einer Warteschlange auf die Freigabe von trau warten. Jedes thread weiß, an welcher Stelle von trau, z.B. am Anfang oder bei wait(), es in die Warteschlange abgestellt wurde und wird später an dieser Stelle seinen Lauf fortsetzen.

  Nach späterem Wecken und Freigabe von trau(String,boolean) für das thread bei wait() muss für dieses thread erneut geprüft werden, ob die Bedingung while(this.isBraut==isBraut) inzwischen erfüllt ist und die Fortsetzung des Laufs gestattet oder ob das thread wieder mit wait() in den Wartezustand versetzt werden muss.

  Die while-Schleife kann nicht durch eine if-Anweisung ersetzt werden, da eine if-Anweisung im Gegensatz zur while-Schleife nicht wiederholt wird und die vielleicht inzwischen abgeänderte Bedingung nicht erneut geprüft wird.

  Direkt vor Verlassen der gemeinsamen Methode trau(String,boolean) weckt das jeweilige thread mit notifyAll() alle wartenden thread.


12.3     Testfragen

zu    Frage                                  | abdeckbare Antwort
---------------------------------------------+--------------------
                                             |
12.1.1  Ergänze WindGarantie um einen thread | String[] Wetter=
      mit "rain" und 200ms !                 |  {"calm ","rain ",
                                             |           "wind "};      
                                             | int[] ms=                                                          
                                             |  {300,200,100};                                             
                                             |
12.1.2   Was  druckt  WindVorrang aus,  wenn | wind wind wind ...
   int[] prio={MIN_PRIORITY,MIN_PRIORITY +1};| 
      durch                                  |
   int[] prio={MIN_PRIORITY,NORM_PRIORITY+1};| 
      ersetzt wird?                          | 
                                             |
12.2.1   Was  druckt  Trauungen   aus,  wenn | AMT oeffnet
      der  Methodenrumpf von forerun() durch | Carla & Carl     
      einen leeren Block {} ersetzt wird?    | Emily & Emil 
                                             | Gerda & Gerd       
                                             | Henny & Hein
                                             | Paula & Paul
                                             | AMT schliesst
                                             |
12.2.1   Ist  while()  in  der  Methode trau | nein,Bedingung muss
      (String,boolean) ersetzbar durch if()? |  mehrmals geprüft
                                             |  werden
                                             |
12.2.1   Ist  wait()  in  der  Methode  trau | nein, notifyAll()
      (String, boolean)    ersetzbar   durch | weckt sleep() nicht
      Thread.sleep(1000) ?                   |
                                             |
12.2.2 Schützt synchronisized(this){...}     | 
    in Tilgungen vor                         | 
     # falschem Überschreiben von stand ?    | ja
     # falschem Einfügen  von Drucktext ?    | ja,folgt aus obigem
                                             |
    Java Programmierung
  Kap.13