Was ist Nebenläufigkeit?

Unter Nebenläufigkeit versteht man die parallele Ausführung von Programmen bzw. die parallele Ausführung einzelner Teile eines Programmes. Die parallele Ausführung von Programmen in einem Betriebssystem wird auch als Multitasking bezeichnet und die parallele Ausführung von Programmteilen wird auch als Multithreading bezeichnet.

Wesentliche Elemente für Nebenläufigkeit sind Prozesse und Threads. Jeder Thread ist dabei einem Prozess zugeordnet, wohingegen ein Prozess mehrere Threads haben kann und in modernen Betriebssystemumgebungen mindestens einen Thread haben muss. Ein weiterer wesentlicher Unterschied ist, dass Prozesse im Speicher voneinander getrennt sind. Ein Prozess kann demnach nicht direkt auf Speicherbereiche eines anderen Prozesses zugreifen. Threads, die den gleichen Prozess haben, teilen sich einen Speicherbereich und können somit gemeinsam auf diesen Speicherbereich zugreifen.

Ein Java-Programm wird immer mit einem Thread, dem main-Thread, gestartet, von dem aus weitere Threads erzeugt werden können. In diesem Artikel wird die Programmierung der Nebenläufigeit in Java mit Threads beandelt. Prozesse spielen in diesem Artikel keine Rolle.

Thread und Runnable

Damit Objekte für die Nebenläufigkeit geeignet sind müssen die entsprechenden Klassen das Interface java.lang.Runnable implementieren. Dies kann durch direkte Implementierung bzw. durch Erweiterung der Klasse java.lang.Thread, welche Runnable implementiert, geschehen. Bei der Erweiterung der Klasse Thread stehen einem mehr Methoden bezüglich der Nebenläufigkeit zur Verfügung. Da in Java nur die Einfachvererbung möglich ist, kann es vorkommen, dass die Erweiterung der Klasse Thread keinen Sinn macht und die Implementierung des Interfaces Runnable zum Einsatz kommen muss, mit Hilfe dessen ein neuer Thread erzeugt wird.

Welche Methode auch zum Einsatz kommt, in den neuen Klassen muss die Methode

void run()

überschrieben/implementiert werden, welche den Einstiegspunkt des Quelltextes darstellt, der nebenläufig ausgeführt werden soll. Gestartet wird ein Thread durch den Aufruf der Methode start() des Thread Objektes. Wird die Methode run() direkt aufgerufen, führt dies dazu, dass die Methode nicht in einem neuen, sondern im aktuellen Thread (also nicht nebenläuufig zum aktuellen Thread) ausgeführt wird.

Das nachfolgende Beispiel zeigt wie, parallel zum main-Thread, zwei weitere Threads ausgeführt werden. Dabei implementiert zur Thread-Erzeugung eine Klasse das Interface Runnable wohingegen eine weitere Klasse Thread als Basisklasse verwendet.

public class ThreadExample1 {
  public static void main(String[] args){
    Thread thread1 = new T1(),
           thread2 = new Thread(new R1());
    thread1.start();
    thread2.start();
    for (int i = 1; i < 6; ++i){
      System.out.println("Main: "+i);
    }    
  }
}

class T1 extends Thread {
  public void run(){
    for (int i = 1; i < 6; ++i){
      System.out.println("T1: "+i);
    }
  }
}

class R1 implements Runnable {
  public void run(){
    for (int i = 1; i < 6; ++i){
      System.out.println("R1: "+i);
    }
  }  
}
itmapa.de - X2H V 0.18


Eine mögliche Ausgabe des Programmes ist:

T1: 1
R1: 1
R1: 2
R1: 3
Main: 1
Main: 2
R1: 4
T1: 2
R1: 5
Main: 3
Main: 4
Main: 5
T1: 3
T1: 4
T1: 5

Ein innerer, anonymer Thread

Manchmal benötigt man einen Thread nur um etwas nebenläufig ablaufen zu lassen, ohne das eine weitere Kommunikation mit dem Thread notwendig ist. Dies ist zum Beispiel der Fall, wenn man einem Fortschrittsdialog die Möglichkeit lassen möchte, sich bei einer längeren Berechnung grafisch zu aktualisieren. Eine einfache Möglichkeit dazu sind innere, anonyme Threads, die direkt nach ihrer Erzeugung gestartet werden. Das nachfolgende Beispiel demonstriert die entsprechende Vorgehensweise.

public class AnonymusThread {
  public static void main(String[] args) {
    new Thread() {
      public void run() {
        for (int i = 0; i < 5; ++i) {
          System.out.println("A: "+i);
          try {
            Thread.sleep(10);
          }
          catch(Exception ex) {
            System.err.println(ex);
            System.exit(1);
          }
        }
      }
    }.start();
    for (int i = 0; i < 5; ++i) {
      System.out.println("B: "+i);
      try {
        Thread.sleep(10);        
      }
      catch(Exception ex) {
        System.err.println(ex);
        System.exit(2);
      }
    }
  }
}
itmapa.de - X2H V 0.18

Die try, catch Blöcke dienen dazu, dass in der kurzen Laufzeit auch ein Wechsel zwischen den beiden Threads vorgenommen wird.

Synchronisation

Manche Programmteile dürfen zu einem Zeitpunkt nur von einem Thread bearbeitet werden. Oder der Zugriff auf gemeinsam genutzte Variablen soll nur von einem Thread zu einem Zeitpunkt geschehen. Um dies zu erreichen muss auf die entsprechenden Elemente synchronisiert zugegriffen werden.

Eine einfache Möglichkeit diese Synchronisation zu erreichen, ist die Verwendung des Schlüsselwortes synchronized, mit Hilffe dessen Programmteile synchronisiert werden können.

Um nun eine Sperre zu realisieren besitzt jedes Objekt einen Monitor. Die Sperre funktioniert immer objektbasierend, dass heißt, wenn ein Objekt die Sperre setzt, sind synchronisierte Bereiche anderer Objekte nicht gesperrt. Wird ein synchronisierter Bereich eines Objektes betreten, wird für das entsprechende Objekt die Sperre gesetzt. Alle anderen Objekte, die einen synchronisierten Bereich des Objektes betreten wollen, müssen solange warten, bis der synchronisierte Bereich wieder verlassen wird und somit die Sperre aufgehoben wird. Das Objekt, welches die Sperre setzt, kann andere synchronisierte Bereiche des Objektes betreten, da es die Sperre auf das Objekt gesetzt hat.

Synchronisiert (gesperrt) können Methoden und Blöcke, wobei bei Blöcken noch angegeben werden muss welches Objekt die Sperre setzt.

Notizen

Schlüsselwort: volatile

Wird das Schlüsselwort volatile für Klassen- oder Instanzvariablen verwendet, bedeuted dies, dass die entsprechende Variable jederzeit, jedem Thread, mit ihrem aktuellen Wert zur Verfügung steht. Optimierungen, wie das Verschieben in den Prozessorregister, für aufwendigere Rechenoperationen, sind demnach nicht möglich.

Die Konsequenz der Verwendung des Schlüsselwortes volatile ist, dass aus Sicht des Programmierers das Schreiben in und das Lesen aus volatile Variablen quasi atomar verläuft, selbst wenn mehrere Operationen für diesen Vorgang erforderlich sind. Das Schreiben und Lesen mit Hilfe von setter- und getter-Methoden verläuft nicht atomar, sondern nur die direkte Wertzuweisung bzw. das Auslesen des Wertes.

Monitor - Lock

Jede Klasse, die synchronized Codeelemente enthält, erhält einen Monitor. Dieser Monitor sorgt dafür, falls ein synchronisierter Abschnitt betreten wird, dass das entsprechende Objekt einen Lock (Sperre) erhält. Im Fall, dass eine Methode synchronisiert ist, werden alle synchronisierten Methoden des Objektes gesperrt. Es ist auch möglich, dass nur bestimmte Abschnitte innerhalb einer Methode synchronisiert werden.

Deadlock

Unter dem Begriff Deadlock versteht man ein Problem wobei zwei odere mehrere Threads sich gegenseitig Resourcen vorenthalten, die für eine weitere Ausführung der beteiligten Threads aber benötigt werden.

Ein Beispiel aus der realen Welt wäre z.B. zwei Kinder die ein Computerspiel spielen möchten, wobei eine bestimmte CD-Rom zum starten des Spiels benötigt wird. Das erste Kind setzt sich vor dem Computer und sucht die CD-Rom zum starten des Spieles. Die CD-Rom aber hat sich zwischenzeitlich das zweite Kind zu eigen gemacht. Beide können mit dem Start des Spiels nicht fortfahren, bis einer von Ihnen eine der Resourcen (Coputer bzw. CD-Rom) freigiebt. Die beiden Kinder stehen nun in einer Deadlock-Beziehung zueinander.

Innerhalb der Softwareentwickung müssen die Resourcen nicht unbedingt physikalischer Natur sein, sondern können z.B. auch benötigte Objekte sein.

Livelock

Livelock ist eine spezille Form des Deadlock. Beim Livelock arbeiten die Threads weiter und verändern ihren Zustand. Jedoch führen die Veränderungen jeweils wieder zu einem Deadlock. Ein Beispiel für einen Livelock sind Personen, die direkt aufeinander zugehen und beide gleichzeitig immer von Links nach Rechts wechseln um aneinander vorbeizukommen. Durch den gleichzeitigen Wechsel kommen die Personen jedoch nicht aneinander vorbei.

Race condition

Unter Race Condition versteht man, das zwei oder mehr Threads unsynchrosiert auf gleiche Variablen lesend und schreibend zugreifen. Da nicht vorherzusehen ist, wann welcher Thread auf eine Variable zugreift, kann das Ergebnis der Operationen nicht vorhergesagt werden.

Fairness

Unter Fairness versteht man, dass jeder ausführbare Thread, egal welcher Priorität, auch zur Ausführung kommt. Bei unendlicher Laufzeit müssen auch alle beteiligten Threads unendlich zur Ausführung kommen.

Starvation

Unter Starvation versteht man einen Zustand, bei dem ein Thread keinen Zugriff auf benötigte Resource erhält, wobei ein oder mehrere andere Threads Zugriff auf die Resourcen haben und diese blockieren.

Mutex

Mit Mutex werden Verfahren bezeichnet, die verhindern, dass mehrere Threads gleichzeitig auf gemeinsam genutzte Variablen zugreifen.

de.wikipedia.org
Mutex
http://de.wikipedia.org/wiki/Mutex

de.wikipedia.org
Monitor (Informatik)
http://de.wikipedia.org/wiki/Monitor_(Informatik)