home  |  suche  |  kontakt/johner  |  institut 
studierende  |  tech-docs  |  mindmailer 

Nebenläufigkeit

Als Nebenläufigkeit bezeichnet man die Fähigkeit eines Systems, zwei oder mehr Vorgänge gleichzeitig oder nebenläufig ("parallel") ausführen zu können. Dabei unterscheidet man von Multitasking und Multithreading. Von Multitasking spricht man, wenn auf einem Rechner mehrere Programme (Prozesse) "parallel" ausgeführt werden (Thread = Folge von Anweisungen). Durch Multithreading kann die Nebeläufigkeit innerhalb (!) eines Prozesses realisiert werden, zum Beispiel wartet die GUI auf einen Mausklick. Multithreading erlaubt es, dass das Programm mehrere CPUs nutzt.

Ein wichtiger Unterschied zwischen Threads und Prozessen ist der, dass alle Threads eines Programms sich einen gemeinsamen Adressraum teilen, also auf dieselben Variablen zugreifen, während die Adressräume unterschiedlicher Prozesse streng voneinander getrennt sind. Dabei hat jeder Thread einen eigenen Stack für das Anlegen von Variablen und zum Aufrufen von Methoden.

Threads teilen sich die Variablen und Objekte des Programms zu denen sie gehören. Eine echte parallele Ausführung von Prozessen kann nur mit mehreren Prozessoren durchgeführt werden. 

Jedes Java-Programm hat einen Main-Thread, der beim Ausführen des Programms für die ausfphrung der main-Methode zuständig ist.

2.7.2 Realisierung von Multithreading in Java

Threads werden in Java durch die Klasse Thread und das Interfaces Runnable implementiert. In beiden Fällen wird der Thread-Body, also der prallel auszuführende Code, in Form der überlagerten Methode run zur Verfügung gestellt.

package parallel;

public class FastCommand implements Runnable {
  public void run(){
    for (int i = 0; i < 1000; i++) {
      System.out.println("I ist " + i);
    }
  }
}
-------------------------------------------------------------------------------------------
package parallel;

import java.text.SimpleDateFormat;
import java.util.Date;

public class SlowCommand extends Thread{
  public void run() { 
    SimpleDateFormat sdf = new SimpleDateFormat("hh:mm:ss");
    for(int i = 0; i < 50; i++){
      Date date = new Date();
      System.out.println("Es ist " + sdf.format(date));
    }
  }
}

Nicht immer ist es möglich, eine Klasse, die als Thread laufen soll, von Thread abzuleiten. Dies ist insbesondere dann nicht möglich, wenn die Klasse Bestandteil einer Vererbungshierrachie ist, die eingentlich nichts mit Multithreading zu tun hat. Da Java keine Mehrfachvererbung kennt, kann eine bereits abgeleitete Klasse nicht von einer weiteren Klasse erben. Um trotzdem das Multithreading-Konzept nutzen zu können wird einfach das Interface Runnable implementiert. Dieses Interface besteht nur aus der run-Methode. Tatsächlich muss jede Klasse, deren Instanzen als Thread laufen sollen , das Interface Runnable implementieren (sogar die Klasse Thread selbst).

Um eine nicht von Thread abgeleitete Instanz als Thread laufen zu lassen, sind folgende Schritte zu befolgen.

  1. Zunächst ein Thread-Objekt erzeugen
  2. An den Konstruktor wird das Objekt übergeben das parallel ausgeführt werden soll
  3. Die Methode start des neuen Thread-Objekts aufrufen

Dieses Beispiel zeigt die Vorgehensweise:

package parallel;

public class StartThreads {
  public static void main(String[] args) {
    FastCommand fastCommand = new FastCommand();
    Thread fast = new Thread(fastCommand);
    fast.start();
  }
}

Arbeiten mit Threads

Die Methode run sollte vom Programm niemals direkt aufgerufen werden. Um einen Thread zu starten, ist immer start aufzurufen! Dadurch wird der neue Thread erzeugt und initialisiert und ruft schließlich selbst run auf, um den Anwendungscode auszuführen. Ein direkter Aufruf von run würde dagegen keinen neuen Thread erzeugen, sonder wäre ein normaler Methodenaufruf wie jeder andere und würde direkt aus dem bereits laufenden Thread des Aufrufers erfolgen.

Es gibt 3 Möglichkeiten, dass ein Thread zuende ist

  • das Ende der run()-Methode ist erreicht
  • run() bricht mit Fehlern ab
  • der Thread wird von außen mit interrupt() unterbrochen, allerdings nur wenn innerhalb des Threads eine Überprüfung stattfindet.

Durch Aufruf von interrupt wird ein Flag gesetzt, das eine Unterbrechungsanforderung signalisiert. Durch Aufruf von isInterrupted kann der Thread feststellen, ob das Abbruchflag gesetzt wurde und der Thread beendet werden soll.

package parallel;

public class Interruptable extends Thread {
  
  public void run() {
    while (!isInterrupted() ) {
      
      System.out.println("Bin noch am Kämpfen");
      
      try {
        Thread.sleep(500);
      catch (InterruptedException e) {
        interrupt();
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
  }

}

Mit der while-Schleife wird überprüft, ob der Thread von außen noch nicht abgebrochen wurde.

Die Bildschirmausgabe in diesem Programm ist vermutlich deutlich kürzer, als die Pause nach der Bildschirmausgabe. Deswegen ist es recht wahrscheinlich, dass der Aufruf von interrupt während des Aufrufs von sleep  erfolgt. Ist das der Fall, wird sleep mit einer Interrupted-Exception abgebrochen (auch wenn die geforderte Zeitspanne noch nicht vollständig verstrichen ist). Wichtig ist hier, dass das Abbruchflag von des Exception zurückgesetzt wird (isInterrupted liefert dann wieder false) und der Aufruf von interrupt somit eigentlich verlorengehen würde, wenn er nicht direkt in der catch-Klausel behandelt würde. Wir rufen daher innerhalb der catch-Klausel interrupt erneut auf, um das Flag wieder auf true zu setzen und run die Abbruchanforderung zu signalisieren.

Bei endlos laufenden Threads besteht die Möglichkeit über die Methode setDeamon(true) zu signalisieren, dass beim Beenden der main() der Thread ebenfalls beendet werden soll.

    Thread infinity = new Thread(new InfinityCommand());
    infinity.setDaemon(true);
    infinity.start();

Synchronisierung von Threads

Zur Synchronisierung nebenläufiger Prozesse hat Java das Konzept des Monitors implementiert. Die Klasse Start gibt den beiden Threads einen gemeinsamen  Monitor mit. Über diesen Monitor (vom Typ Object) können sich die beiden Threads mit Hilfe der Methoden wait() und notify() verständigen. Die Methoden wait() und notify() erbt jede Klasse von Object. Beide Methoden müssen in einem Block stehen, der synchornized ist.

Durch synchronized kann entweder eine komplette Methode oder ein Block innerhalb einer Methode geschützt werden. Das folgende Codebeispiel zeigt einen Thread, der einen Teil der run-Methode über das Monitor-Objekt synchonisiert. Das Monitor-Objekt wird ihm im Konstruktor übergeben. Ein anderer Thread, der mit diesem Thread zusammenarbeitet, muss das gleiche Objekt ebenfalls im Konstruktor übergeben bekommen.

package parallel2;

public class BerechnerClient extends Thread {
  private Object monitor;

  public BerechnerClient(Object monitor) {
    this.monitor = monitor;
  }
  
  public void run() {
    
    try {
      synchronized (monitor) {
        System.out.println("Ich, der Client, warte auf den Berechner");
        monitor.wait();
        System.out.println("Endlich, ich, der Client, habe Nachricht bekommen");
      }
    catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
  

}

Synchronisierung von Threads mit Java 5

In Java 5 läuft die Synchronisation nicht mehr über ein normales Object, sondern über die spezielle Klassen java.util.concurrent.locks.Condition und java.util.concurrent.locks.Lock.

  • Condition wird von Lock erzeugt
  • condition.await() ist Pendant zu object.wait()
  • condition.signal() ist Pendant zu object.notify()

signal() und wait() müssen im synchronisierten Block innerhalb des locks stehen. Die Synchronisation erfolgt über den Aufruf der Methode lock.lock() und beim Beenden lock.unlock().

package parallel3;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class BerechnerClient extends Thread {
  private Lock lock;
  private Condition condition;

  public BerechnerClient(Lock lock, Condition condition) {
    this.lock = lock;
    this.condition = condition;
  }

  public void run() {
    
    try {
        lock.lock();
        System.out.println("Ich, der Client, warte auf den Berechner");
        condition.await();
        System.out.println("Endlich, ich, der Client, habe Nachricht bekommen");
        lock.unlock();
      
    catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }

}