Einleitung

Bearbeiten

Nebenläufigkeit (concurrency) ist die Fähigkeit eines Systems, zwei oder auch mehrere Aufgaben (scheinbar) gleichzeitig auszuführen. In Java kann die Ausführungsparallelität innerhalb eines Programmes mittels Threads (lightweight processes) erzwungen werden. Laufen mehrere Threads parallel, so spricht man auch von Multithreading.

Threads erzeugen und starten

Bearbeiten

Threads sind Bestandteil des Java-Standardpackages java.lang.

Methode 1: Die Thread-Klasse

Bearbeiten

Die Klasse Thread implementiert die Schnittstelle Runnable.

Prinzipielle Vorgehensweise:

  • Eine Klasse, abgeleitet von Thread, erstellen
  • Die Thread-Methode public void run () überschreiben
  • Instanz(en) der Thread-Subklasse bilden
  • Die Thread-Instanz(en) mittels public void start() starten

Beispiel:

public class SimpleThread extends Thread
{
  public void run()
  {
    for (int i = 0; i<=100; i++)
    {
      System.out.println(getName() + ": " + i);

      try
      {
        sleep(50);
      }
      catch(InterruptedException ie)
      {
        // ...
      }
    } 
  }
}

public class App
{
  public static void main(String[] args)
  {
    SimpleThread thread1 = new SimpleThread();
    SimpleThread thread2 = new SimpleThread();
 
    thread1.start();
    thread2.start();
  }
}

oder

public class SimpleThread extends Thread
{
  public SimpleThread(String id)
  {
    start();
  } 


  public void run()
   {
    for (int i = 0; i<=100; i++)
    {
      System.out.println(getName() + ": " + i);

      try
      {
        sleep(50);
      }

      catch(InterruptedException ie)
      {
        // ...
      }
    } 
  }
}

public class App
{
  public static void main(String[] args)
  {
    new SimpleThread("1");
    new SimpleThread("2");
  }
}

Methode 2: Das Runnable-Interface

Bearbeiten

Prinzipielle Vorgehensweise:

  • Eine Runnable implementierende Klasse erstellen
  • Die Runnable-Methode public void run () überschreiben
  • Instanz(en) der Runnable implementierenden Klasse bilden
  • Thread-Instanz(en) erstellen. Als Parameter wird eine Runnable-Instanz übergeben.
  • Die Thread-Instanz(en) mittels public void start() starten
public class SimpleRunnable implements Runnable
{ 
  public void run()
  { 
    for (int i = 0; i<=100; i++)
     {
      System.out.println(Thread.currentThread().getName() + ": " + i);

      try
      {
        Thread.sleep(50);
      }

      catch(InterruptedException ie)
      {
        // ...
      }
    } 
  }
}


public class App
{
  public static void main(String[] args)
  {
    Runnable r1 = new SimpleRunnable();
    Runnable r2 = new SimpleRunnable();

    new Thread(r1).start();
    new Thread(r2).start();    
  }
}

Der main-Thread

Bearbeiten

Jede Java-Applikation besitzt zumindest einen Thread, den main-Thread.

public class App
{
  public static void main(String[] args)
  {
    Thread t = Thread.currentThread();

    System.out.println("Name = " + t.getName());
    System.out.println("Id = " + t.getId());
    System.out.println("Priorität = " + t.getPriority());
    System.out.println("Zustand = " + t.getState());
  }
}

zeigt

Name = main
Id = 1
Priorität = 5
Zustand = RUNNABLE


Thread-Zustände

Bearbeiten

Threads können in verschiedenen Zuständen vorliegen:

Thread.State Erläuterung
NEW erzeugt, aber noch nicht gestartet
RUNNABLE lauffähig; wird in der Java Virtual Machine (JVM) ausgeführt oder wartet auf die Freigabe von Ressourcen
BLOCKED geblockt; wartet auf einen Monitor-Lock (siehe Synchronisation)
WAITING wartet; das ist der Fall wenn eine der folgenden Methoden aufgerufen wurde:

Object.wait(...) ohne Timeout
Thread.join(...) ohne Timeout
LockSupport.park()

TIME_WAITING wartet eine definierte Zeitspanne; das ist der Fall wenn eine der folgenden Methoden aufgerufen wurde:

Thread.sleep(...)
Object.wait(...) mit Timeout
Thread.join(...) mit Timeout
LockSupport.parkNanos(...)
LockSupport.parkUntil(...)

TERMINATED beendet; eine einmal beendete Thread-Instanz kann nicht mehr erneut gestartet werden

Abfragen kann man den Thread-Zustand mit der bereits in vorhergehenden Abschnitt verwendeten Funktion

Thread.State.getState().

Threads beenden

Bearbeiten

Das stop-Chainsaw Massacre

Bearbeiten

Die Klasse Thread implementiert die Funktion void stop() zur manuellen Beendigung eines Threads. Diese Funktion ist problembehaftet, daher als deprecated gekennzeichnet und soll nicht benutzt werden.

Der run-Suizid

Bearbeiten

Implementiert man in der run()-Methode keine Endlosschleife, dann löst sich das Problem durch Zeitablauf von selbst.

public class SimpleThread extends Thread
{ 
  public void run()
  { 
    for(int i = 0; i<=100; i++)
    {
      System.out.println(getState());
    } 
  }
}


public class App
{
  public static void main(String[] args)
  {
    SimpleThread t = new SimpleThread();    
    t.start();

    try
    {
      Thread.sleep(1000);      
    }
    catch(InterruptedException ie)
    {
    }

    System.out.println(t.getState());
  }
}

ergibt

...
RUNNABLE
RUNNABLE
TERMINATED

Flag-Methode

Bearbeiten
public class SimpleThread extends Thread
{    
  volatile boolean running = false;  // Flag

  public void stopIt() 
  {
    running = false;
  }
  
  public void run()
  {    
    running = true;

    while(running == true)
    {
      System.out.println(getState());
    }   
  }
}


public class App
{
  public static void main(String[] args)
  {
    SimpleThread t = new SimpleThread();    
    t.start();

    try
    {
      Thread.sleep(1000);
    }
    catch(InterruptedException ie)
    {
      // ...
    }

    t.stopIt();    
  }
}

Referenz-Methode

Bearbeiten

Eine Variante der Flag-Methode ist hier angegeben. Sie benutzt kein Extra-Flag sondern stattdessen die Referenz zum Thread.

public class Applet implements Runnable
{
  Thread thread;
  
  public void start()
  {
    thread = new Thread(this);
    thread.start();
  }
  
  public void stop()
  {
    thread = null;
  }
  
  public void run()
  {
    Thread myThread = Thread.currentThread();
    while (thread == myThread)
    {
      // tu etwas
    }
  }
}

Interrupt

Bearbeiten

Mit Hilfe der Thread-Methoden

void interrupt()

und

boolean isInterrupted()

können wir auch einen Thread beenden.

public class SimpleThread extends Thread
{     
  public void run()
  {    
    while(isInterrupted() == false)
    {
      System.out.println(getState());
    }   
  }
}


public class App
{
  public static void main(String[] args)
  {
    Thread t = new SimpleThread();    
    t.start();

    try
    {
      Thread.sleep(1000);
    }
    catch(InterruptedException ie)
    {
      // ...
    }

    t.interrupt();    
  }
}

Threads anhalten

Bearbeiten

Ein Thread kann mit den Funktionen

static void sleep(long millis)

static void sleep(long millis, int nanos)

für millis Millisekunden (+ nanos Nanosekunden) angehalten werden. Diese Funktion kennen wir schon aus früheren Codebeispielen.

Zusätzlich kann anderen wartenden Threads der Vortritt gewährt werden.

static void yield()

zum temporären Pausieren des momentan ausgeführten Threads, um andere Threads ausführen zu können.

public class SimpleThread extends Thread
{  
  public void run()
  {         
    for(int i = 0; i<=1000; i++)
    {
      System.out.println(getName() + ": " + i);

      if(i%2 == 0)
      {
        yield();
      }
    }   
  }
}


public class App
{
  public static void main(String[] args)
  {
    new SimpleThread().start();
    new SimpleThread().start();
    new SimpleThread().start();
  }
}

ergibt

...
Thread 0: 9
Thread 0: 10
Thread 1: 9
Thread 1: 10
Thread 2: 9
Thread 2: 10
...

Auf das Ende eines Threads warten

Bearbeiten

void join()

Warte auf das Ende eines Threads

void join(long millis)

void join(long millis, int nanos)

Warte längstens millis Millisekunden (+ nanos Nanosekunden) auf das Ende eines Threads. Übergibt man als Parameter 0, so bedeutet dies, dass beliebig lange auf das Ende des Threads gewartet wird.

public class SimpleThread extends Thread
{  
  public void run()
  {    
    for(int i = 0; i<=1000; i++)
    {
      System.out.println(getState());
    }   
  }
}
 

public class App
{
  public static void main(String[] args)
  {
    SimpleThread t = new SimpleThread();    

    t.start();

    try
    {
      t.join();      
    }
    catch(InterruptedException ie)
    {
      // ..
    }

    System.out.println(t.getState());
  }
}

ergibt

...
RUNNABLE
RUNNABLE
TERMINATED

Thread-Priorität

Bearbeiten

Durch Anwendung der Thread-Methode

void setPriority(int newPriority)

kann die Priorität eines Threads im Bereich von 1 (Thread.MIN_PRIORITY) bis 10 (Thread.MAX_PRIORITY) geändert werden. Ein Thread erhält zuerst immer die Priorität des Threads, in dem er gestartet wurde. Der main-Thread weist standardmäßig die Priorität 5 auf. Die konkrete Umsetzung der zugewiesenen Priorität hängt dabei sehr stark vom jeweiligen Betriebssystem ab.

Die Abfrage der momentanen Thread-Priorität geschieht mittels der Thread-Methode

int getPriority()


Scheduler

Bearbeiten

Die Ausführungsplanung zum Umschalten zwischen aktiven Threads und Prozessen nennt man Scheduling. Mögliche Scheduling-Strategien:

  • Prioritätssteuerung (Preemption): Es wird immer der Thread mit der höchsten Priorität ausgeführt
  • Zeitsteuerung (Time-Slicing): Der Scheduler weist den einzelnen Threads Zeitabschnitte zu, während der sie zur Ausführung gelangen
  • Prioritäts- und Zeitsteuerung kombiniert

Dämonen

Bearbeiten

Ein Dämon ist ein Thread der im Hintergrund ausgeführt wird. Mit der Thread-Methode

void setDaemon(boolean on)

kann zwischen den Thread-Typen Dämon-Thread (on = true) und konventionell im Vordergrund laufendem Thread (off = false) umgeschaltet werden.

Abfragen kann man den Thread-Typ mittels

boolean isDaemon()

Nicht jeder Thread eignet sich zum Dämon-Thread. Es gilt folgende Regel: Eine Java-VM beendet sich, wenn keine Nicht-Dämon-Threads mehr laufen.

Ein prominenter Dämon ist übrigens der Garbage Collector - es würde auch wenig Sinn ergeben, wenn er weiter arbeiten würde, nachdem ein Programm zu Ende ist.

Threadgruppen

Bearbeiten

Threads kann man auch gruppieren. Beim Start einer Applikation wird automatische eine main-Threadgruppe angelegt. Der main-Thread ist Teil dieser Gruppe. All Threads sind automatisch Bestandteil einer Threadgruppe.

public class SimpleThread extends Thread
{  
  SimpleThread(ThreadGroup tg, String name)
  {
    super(tg, name);
  }

  public void run()
  {         
    try
    {
       sleep(5000);
    } 
    catch(InterruptedException ie)
    {
      // ...
    }
  }
}


public class App
{
  public static void main(String[] args)
  {     
    ThreadGroup tg = new ThreadGroup("Testgruppe");
    Thread t1 = new SimpleThread(tg, "t1");
    Thread t2 = new SimpleThread(tg, "t2");

    t1.start();
    t2.start();

    Thread array[] = new Thread[tg.activeCount()];

    tg.enumerate(array);

    for(Thread t: array)
    {
      System.out.println(t.getName() + " ist Gruppenmitglied von " + tg.getName() );
    }
  }
}

Threads synchronisieren

Bearbeiten

Race Conditions

Bearbeiten

Definition (aus Wikipedia, der freien Enzyklopädie vom 27.08.2005):

Als Race Condition (zu deutsch Wettlaufsituation oder Wettkampfbedingung) bezeichnen Programmierer Konstellationen, in denen das Ergebnis einer Operation vom zeitlichen Verhalten bestimmter Einzeloperationen abhängt. Unbeabsichtigte Race Conditions sind ein häufiger Grund für schwer auffindbare Programmfehler, so genannte Heisenbugs. ...

Genau solche Race Conditions können auch bei Threads vorkommen. Greifen mehrere Threads auf bestimmte Ressourcen (Dateien, Variablen, Datenbanken, Drucker, etc.) zu, so kann sich der Programmierer nicht darauf verlassen, dass die Threads dies immer in einer bestimmten Reihenfolge und kollisionsfrei tun. Das hängt auch von Faktoren ab, die der Programmierer nicht beeinflussen kann, zum Beispiel der Scheduling-Strategie oder der konkreten Umsetzung der gewünschten Thread-Priorität.

Deshalb muss eine Programmiersprache Mechanismen bereitstellen, um derartige Probleme zu lösen. Eine Methode wird als thread-sicher bezeichnet, wenn sie bedenkenlos von Threads aufgerufen werden kann.

Atomare Operationen

Bearbeiten

Definition (aus Wikipedia, der freien Enzyklopädie vom 27.08.2005):

Eine Atomare Operation [...] bezeichnet eine Operation im Computer, welche durch keine andere Operation unterbrochen werden kann. Atomare Operationen sind wichtig beim Synchronisieren von Daten. ...

Auch diesen Aspekt muss man beim Programmieren mit Threads beachten (Stichwort: Transaktionen bei Datenbanken).

Selbst bei einfachen Variablen kann man sich nicht auf atomares Verhalten verlassen. Um sicherzustellen, dass Objekt- oder Klassenvariablen vor jedem Zugriff auf den aktuellen Stand gebracht werden verwendet man das Schlüsselwort volatile (flüchtig, launisch, unbeständig).

volatile long l;

Das Schlüsselwort synchronized

Bearbeiten

Greifen mehrere Threads auf dieselben Ressourcen zu, so kann es zwecks Problemvermeidung notwendig sein Threads zu synchronisieren.

Eine Methode können wir durch das Schlüsselwort synchronized kennzeichnen

 synchronized void xxx()
 {
   // ...
 }

Die VM wird diese Methode nun bei Bedarf automatisch sperren und entsperren.

Auch einzelne Code-Blöcke können synchronisiert werden

synchronized(objekt)
{
  // ...
}

Monitore

Bearbeiten

Die JVM definiert für Synchronisationszwecke sogenannte Monitore. Jedes Objekt mit synchronisiertem Code ist in Java ein Monitor. Dieser Monitor besitzt einen Monitor-Lock (Lock, Sperre) und führt eine Warteliste von Threads die ausgesperrt wurden. Beendet ein Thread eine synchronized-Methode oder einen synchronized-Block, so wird die Sperre aufgehoben und der nächste Thread kommt zum Zug. Der Aufruf von sleep() oder yield() hebt eine solche Sperre allerdings nicht auf.

Deadlocks

Bearbeiten

Ein Problem bei Synchronisation können sogenannte Deadlocks (Verklemmungen) darstellen. Dabei sperren sich zwei oder mehrere Threads gleichzeitig von benötigten Ressourcen aus. Thread A wartet darauf, dass Thread B eine Ressource freigibt. Gleichzeitig wartet aber Thread B, dass Thread A seine gesperrte Ressource freigibt. Setzt man im Vorfeld keine geeigneten Maßnahmen, dann werden die beiden Threads ewig warten und mit den Threads auch der genervte und ratlose Programmbenutzer.

Zur Erkennung von Deadlock-Situationen können Deadlock-Detection-Utilities hilfreich sein. Aktuelle Java-Releases und auch IDEs wie zum Beispiel Eclipse 3.1 bieten derartige Möglichkeiten.

Das wait-notify-Konzept

Bearbeiten

Mit Hilfe der Object-Methode

public final void wait()

können Threads in einen Wartezustand versetzt werden. Sie geben dann den Monitor frei.

Besitzt ein Thread den Monitor eines Objektes, so kann er durch

void notify()

oder

void notifyAll()

wartende Threads benachrichtigen und aus dem Wartezustand erlösen.

Concurrent-Programming ab Java 5.0

Bearbeiten

Ab Java 5.0 werden in den Packages

  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks

zusätzlich zur konventionellen Thread-Programmierung weitere Möglichkeiten für nebenläufiges Programmieren bereitgestellt. Nachfolgend werden einführend in diese Thematik ganz kurz ein paar Klassen und Möglichkeiten dieser Pakete angesprochen.

Callable

Bearbeiten

Das Interface Callable<> dient ähnlichen Zwecken wie das Interface Runnable, ist aber ein bisschen flexibler. Außerdem ist anstelle der run-Methode die call-Methode zu implementieren .

import java.util.concurrent.*;

public class CallableThread implements Callable<Integer> 
{
  private int i;

  CallableThread(int i)
  {
    this.i = i;
  }

  public Integer call()
  {
    for (int j=0; j<=1000; j++)
    {
      System.out.println("Thread " + i + ":" + j);
    }

    return i;
  }
}

Executors

Bearbeiten

Die Klasse Executors enthält Fabriks- und Hilfsmethoden für

  • Callable
  • Executor
  • ExecutorService
  • ScheduledExecutorService
  • ThreadFactory

Future und FutureTask

Bearbeiten

Die Schnittstelle Future<> implementiert Runnable. Die Klasse FutureTask<> implementiert Future<>.

Thread-Pools

Bearbeiten

Zwecks optimaler Performance kann die Kreierung von Thread-Pools sinnvoll sein. Thread-Pools fassen Threads zu gemanagten Kollektionen zusammen.

import java.util.concurrent.*;

public class App
{
  public static void main(String[] args)
  {     
    ExecutorService es = Executors.newCachedThreadPool();
    FutureTask<Integer> f1 = new FutureTask<Integer>(new CallableThread(1));
    FutureTask<Integer> f2 = new FutureTask<Integer>(new CallableThread(2));

    es.execute(f1);
    es.execute(f2);
  }
}

Zeitgesteuerte Task-Ausführung

Bearbeiten

TimerTask implementiert Runnable und kann ein- oder mehrmalig durch einen Timer ausgeführt werden.

import java.util.*;

public class Task extends TimerTask 
{
  public void run()
  {
    System.out.println("Hallo!"); 
  }
}


public class App
{
  public static void main(String[] args)
  {
    Timer timer = new Timer();
    timer.schedule(new Task(), 1000, 2000);
  }
}

Die schedule()-Methode gibt es mit unterschiedlichen Signaturen. Im Beispiel wurde eine Initialverzögerung (delay) von 1000ms und eine Wiederholung (period) alle 2000ms gewählt.