Java Thread
Willemers Informatik-Ecke
Properties Java

Auf Betriebssystemebene sind wir es gewohnt, dass mehrere Prozesse parallel laufen. Die Möglichkeit, parallele Abläufe zu steuern, wünschen wir uns auch in unseren Programmen. Dafür gibt es Leichtgewichtsprozesse oder Threads. Während Prozesse ihre Datenspeicher gegeneinander schützen (zwei parallel gestartete Textverarbeitungen verarbeiten völlig unterschiedliche Texte), greifen Threads ungeschützt auf die Ressourcen parallel zu.

Zu dem Thema gibt es zwei Videos auf Youtube

Schlafen für Profis

Bevor wir uns mit parallelen Abläufen befassen, betrachten wir professionelles Warten. Bei parallelen Abläufen muss immer wieder einer Warten. In früheren Zeiten (C-64) hat man eine Endlosschleife rotieren lassen, um das Programm zu verzögern. In Java sähe das etwa so aus:
while (!fertig) {
    // nichts tun: busy waiting oder polling! Böse!
}
Die C-64-Lösung kocht und blockiert den Prozessor. Er ist ständig beschäftigt, weil er in der Schleife herumspringt und die Endebedingung prüft. So etwas nennt man Polling oder Busy Waiting und gehört auf den Müllhaufen der Geschichte.

Stattdessen bieten alle Systeme einen Mechanismus Sleep an. Sleep sorgt für entspanntes Warten, weil der Thread inaktiv wird und anderen Threads den Prozessor überlässt.

try {
    Thread.sleep(1000); // 1000ms!
} catch (InterruptedException e) {
}
Gelegentliches Schlafen entspannt sogar Abläufe, die einen Computer unter Last setzen. Dazu reichen oft Millisekunden, die an der richtigen Stelle in die Schleife gesetzt werden. Damit bekommen alle anderen Prozesse etwas Luft und der Computer wird wieder bedienbar. Würden sich alle Programmierer von Werbebannern daran halten, gäbe es vielleicht keine Werbeblocker.

Das folgende Programm nutzt Sleep und zählt damit sekundenweise herunter.

public class CountDown {
    public static void main(String[] args) {
        for (int i = 10; i > 0; --i) {
            System.out.println(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }
}

Thread, der Hintergrundarbeiter

Nun sollen endlich parallele Leichtgewichtsprozesse geschrieben werden. Die Klasse Thread implementiert die Methode run. Darin wird der parallel laufende Code geschrieben.

Wenn Sie also die Klasse Thread erweitern und die Methode run überschreiben, können Sie darin den Code für Ihren Leichtgewichtsprozess ablegen.

import java.util.ArrayList;
import java.util.List;

public class GlockenThread extends Thread {

    private String meldung = "";
    private int anzahl = 0;
    private long zeit = 0;
    
    public GlockenThread(String meldung, int anzahl, long zeit) {
        this.meldung = meldung;
        this.anzahl = anzahl;
        this.zeit = zeit;
    }
    
    @Override
    public void run() {
        for (int i=0; <anzahl; i++) {
            try {
                Thread.sleep(zeit);
                System.out.println(meldung);
            } catch(InterruptedException e) {    
            }
        }
    }
    
    static Lis<Thread> gelaeut = new ArrayLis<Thread>();
    
    public static void main(String[] args) {
        Thread thread = new GlockenThread("bing", 10, 600);
        thread.start();
        thread = new GlockenThread("  bang", 15, 400);
        thread.start();
    }
}
Das Thread-Objekt wird durch Aufruf der Methode start gestartet. Das Beispiel zeigt ein Glockengeläut. Jede Glocke läutet unabhängig von den anderen Glocken, wie es in einem Kirchturm so üblich ist.

Ein Leichtgewichtsprozess (Thread) endet mit dem Erreichen des Endes der Methode run.

Die Methode main bildet den Haupt-Thread. Das bedeutet, dass jedes Programm immer mindestens einen Thread besitzt. Ein Programm endet, wenn der letzte Thread des Programms endet. Das muss nicht der Haupt-Thread sein.

Das Interface Runnable

Nicht immer ist es so einfach, eine Klasse von Thread abzuleiten, da Java-Klassen nur von einer Klasse abgeleitet werden dürfen. Als Alternative zum Erweitern von Thread bietet sich die Implementierung des Interfaces Runnable an. Die Vorgehensweise:
  1. Das Interface Runnable implementieren, insbesondere dessen Methode run
  2. Die Runnable-Implementierung als Parameter dem Konstruktor von Thread übergeben
  3. Das Thread-Objekt über dessen Methode start starten
Das folgende Listing realisiert das Glockengeläut als Implementierung von Runnable:
import java.util.ArrayList;
import java.util.List;

public class Glocken implements Runnable {

    private String meldung = "";
    private int anzahl = 0;
    private long zeit = 0;
    
    public Glocken(String meldung, int anzahl, long zeit) {
        this.meldung = meldung;
        this.anzahl = anzahl;
        this.zeit = zeit;
    }
    
    @Override
    public void run() {
        for (int i=0; i<anzahl; i++) {
            try {
                Thread.sleep(zeit);
                System.out.println(meldung);
            } catch(InterruptedException e) {    
            }
        }
    }
    
    public static void main(String[] args) {
        (new Thread(new Glocken("bing", 10, 600))).start();
        (new Thread(new Glocken("  bang", 15, 400))).start();
        (new Thread(new Glocken("    bong", 8, 800))).start();
    }
}

Threadkontrolle ist ein Kinderspiel - oder nicht?

Wer mit Threads zu tun hat, muss sie auch bändigen. Manchmal verhalten sie sich wie kleine Kinder. Es können folgende Situationen auftreten: Es mag sein, dass nicht alle Pädagogen einverstanden sind, dass Kinder so erzogen werden sollen, bei den Threads funktioniert es.

Auf das Thread-Ende warten (join)

Kommen wir auf das Programm Glocken zurück. Sollte der Pastor bei seiner Begrüßung auf das Ende der letzten Glocke warten wollen, müsste er die Methode main in folgender Weise anpassen:
static List gelaeut = new ArrayList();
    
public static void main(String[] args) {
    gelaeut.add(new Thread(new Glocken("bing", 10, 600)));
    gelaeut.add(new Thread(new Glocken("  bang", 15, 400)));
    gelaeut.add(new Thread(new Glocken("    bong", 8, 800)));
    for (Thread thread : gelaeut) {
        thread.start();
    }
    for (Thread thread : gelaeut) {
        try {
            thread.join();
        } catch (InterruptedException e) {
        }
    }
    System.out.println("Guten Morgen, liebe Gemeinde!");
}
Die Threads werden hier in einer ArrayList abgelegt. Die Elemente der Liste werden zunächst durchgestartet. In der zweiten Schleife werden die Threads wieder eingesammelt. Sollte ein Thread noch laufen, blockiert der Aufruf von join den Aufrufer. Ist der Thread bereits beendet, läuft join einfach durch.

Kritischer Bereich

Es gibt Situationen, in denen parallele Threads nicht gleichzeitig auf eine Ressource zugreifen dürfen. Dies bezeichnet man als kritischen Bereich. Wir verwenden als Beispiel eine Vasenfabrik mit mehreren Öfen, aber nur eine Verpackungsmaschine. Problematisch wird es, wenn mehrere Vasen gleichzeitig an der Verpackungsmaschine ankommen.

Die Klasse VasenOfen produziert eine Anzahl von Vasen. Das benötigt Zeit. Im Beispiel wurde die Zeit zufällig zwischen 0 und 100 ms angesetzt. Dann ruft sie den Verpacker zum verpacken der Vase.

public class VasenOfen implements Runnable {

    private int anzahl;
    private Verpacker verpacker;
    
    public VasenOfen(Verpacker verpackung, int anzahl) {
        this.verpacker = verpackung;
        this.anzahl = anzahl;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < anzahl; i++) {
            // Von Zeit zu Zeit wird eine Vase fertig
            try {
                Thread.sleep((long) (100 * Math.random()));
            } catch (InterruptedException e) {
            }
            // Verpacken ist ein kritischer Bereich!
            verpacker.verpacke();
        }
    }
}
Die Klasse Verpacker simuliert eine Verpackungsmaschine, die die produzierten Vasen der Öfen in Karton einpackt. Sollten eine Vase eintreffen, während eine andere verpackt wird, gibt es Scherben.
public class Verpacker {

    private boolean bereitsAktiv = false;
    
    public void verpacke() {
        if (bereitsAktiv) {
            System.out.println("Klirr");
        } else {
            bereitsAktiv = true;
            System.out.print("Verpacke...");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }    
            System.out.println("...Verpackt");
        }
        bereitsAktiv = false; // fertig!
    }
    
}
Nun wird alles in der Klasse VasenMain gestartet.
public class VasenMain {

    public static void main(String[] args) {
        Verpacker verpacker = new Verpacker();
        for (int i=0; i<3; i++) {
            (new Thread(new VasenOfen(verpacker, 10))).start();
        }
    }
}
Auf dem Bildschirm erscheinen die Meldungen des Verpackers "Verpacke..." und "...Verpackt", aber hin und wieder auch "Klirr" als Zeichen dafür, dass nun mehr als eine Vase in einen Karton passt.

Der Zugang zur Verpackung ist ein kritischer Bereich: Es darf nur ein Thread gleichzeitig den Bereich betreten. Dafür kennt Java das Zauberwort synchronized. Es wird als Modifizierer vor die Methode verpacke gestellt und schon darf immer nur ein Thread gleichzeitig die Methode betreten.

public synchronized void verpacke()
{
    // ...
}
Wenn Sie dieses Wort in der Klasse Verpacker einfügen, gibt es keine Scherben mehr.

Auf Objekte warten (wait/notify)

Ein anderes Szenario ergibt sich, wenn auf ein Ereignis gewartet werden muss. Für ein solches Warten wird ein Objekt als Ankerpunkt verwendet. Bereits die Mutter aller Klassen Object bietet die Methoden wait und notify an. Entsprechend kann jedes beliebige Objekt zum Treffpunkt werden.

Der Aufruf von wait legt den Aufrufer schlafen bis ein anderer Thread notify aufruft. Das Warte-Objekt ist aber selbst wieder ein kritischer Bereich und muss darum vor einem Aufruf von wait oder notify synchronized gesetzt werden. Syntaktisch sieht dies ähnlich wie ein if oder ein while aus:

synchronized(obj) {
    obj.wait();
}
// ...
synchronized(obj) {
    obj.notify();
}
Betrachten wir als Beispiel wieder die Vasenfabrik. Dort benötigt der Verpacker für jede Vase einen Karton. Sind die Kartons verbraucht, muss er auf neue Kartons warten. Solange stoppt die Produktion.

Simuliert wird das durch die Methoden neuerKarton und verbraucheKarton in der Klasse Karton:

public class Karton {
    private int kartons = 0;
    
    public void neuerKarton(int anzahl) {
        synchronized(this) {
            kartons += anzahl;
            this.notify();
       }
    }
    
    public void verbraucheKarton() {
        synchronized(this) {
            while (kartons<=0) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            kartons--;
        }
    }
}
Die Klasse Verpacker wird um eine Zeile verändert, in der ein Karton verbraucht wird. Damit Verpacker aber weiß, wo die Kartons herkommen, wird ihm im Konstruktor ein Objekt von Karton übergeben.
public class Verpacker {

    private boolean bereitsAktiv = false;
    private Karton karton;
    
    public Verpacker(Karton karton) {
        this.karton = karton;
    }

    public synchronized void verpacke() {
        if (bereitsAktiv) {
            System.out.println("Klirr");
        } else {
            bereitsAktiv = true;
            karton.verbraucheKarton();
            System.out.print("Verpacke...");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }    
            System.out.println("...Verpackt");
        }
        bereitsAktiv = false; // fertig!
    }
}
Die Lieferung der Kartons übernimmt der Chef, also main, in der Methode liefereKartons. Der hat eh nichts zu tun. Insofern simuliert das Programm die Wirklichkeit.
public class VasenMain {

    private static void liefereKartons(Karton karton) {
        for (int i=0; i<10; i++) {
            System.out.println("Liefere Kartons");            
            karton.neuerKarton(4);
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
            }
        }        
    }
    public static void main(String[] args) {
        Karton karton = new Karton();
        Verpacker verpacker = new Verpacker(karton);
        for (int i=0; i<3; i++) {
            (new Thread(new VasenOfen(verpacker, 10))).start();
        }
        liefereKartons(karton);
    }
}

Statt warten auf Godot: Time-Out

Immer wieder muss ein Programm auf Ereignisse warten, dessen Ausgang nicht sicher ist. Insofern kann es wie beim Theaterstück Warten auf Godot dazu führen, dass das Ereignis nie eintritt. Während das im Theater offenbar einen unterhaltsamen Abend garantiert, gilt es in der Informatik, diese Situation zu vermeiden. Wartet ein Programm auf eine Internet-Verbindung, wird es nach einer gewissen Zeit das Warten abbrechen. Das nennt man einen Time-Out.

Wir könnten auch das in der Vasenfabrik simulieren. Wenn dort die Uhr Feierabend anzeigt, unterbricht der Verpacker alles und geht nach Hause. Die Uhr unterbricht (interrupt) also die Tätigkeit des Verpackers.

Damit es nicht unübersichtlich wird, zeigt das Programm TimeOut diesen Unterbrechungsmechanismus anhand eines Threads, der auf ein Objekt wartet.

public class TimeOut implements Runnable {

    static private Object object = new Object();
    
    @Override
    public void run() {
        try {
            // Wir können nur auf ein Objekt zugreifen,
            // wenn wir es zuvor synchronisieren
            synchronized(object) {
                object.wait();
            }
            System.out.println("Ich ende regulär");
        } catch (InterruptedException e) {
            System.out.println("Ich wurde unterbrochen");
        }
    }
    
    public static void main(String[] args) {
        Thread thread = new Thread(new TimeOut());
        thread.start();
        try {
            Thread.sleep(200);
            // Unterbreche den Hintergrund-Thread:
            //thread.interrupt();
            Thread.sleep(200);
            // Löse das Ereignis aus, auf das der Thread wartet
            synchronized(object) {
                object.notify();
            }
        } catch (InterruptedException e) {
        }
    }
}
Durch Entkommentieren der Zeile thread.interrupt(); wird der Interrupt ausgelöst. Der Thread erfährt eine InterruptedException und kann auf diese reagieren, indem er endet. Ansonsten läuft der Thread ununterbrochen zu Ende.

Timer in den GUI-APIs: Beispiel Swing

Alle Fensterprogramme arbeiten ereignisorientiert. Sie warten darauf, dass der Benutzer mit der Maus klickt, dass eine Taste gedrückt wird oder das System erzählt, dass ein Fenster neu gezeichnet werden muss. Dazu muss der Nachrichtenkanal frei sein. Ist er es nicht, reagiert das Programm auf keinen Klick und keine Systemnachrichten mehr.

Das zeigt das folgende Beispiel, in dem der Geheimagent im letzten Moment die tickende Zeitbombe entschärft. Der erste Klick startet die Countdown. Danach kommt das Programm aber erst wieder zum Zuge, wenn die Methode countdown zum Ende kommt. Der Geheimagent hat keine Chance, die Zeitbombe zu entschärfen.

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;

/**
 * Ein Countdown ohne Thread blockiert die JFrame-Anwendung
 */

public class ZeitbombeBlockiert extends JFrame implements ActionListener {

    JLabel label = new JLabel("Tick");
    JButton button = new JButton("Start");
    boolean scharf = false;

    public void countdown() {
        for (int i = 10; i > 0; --i) {
            label.setText("" + i);
            System.out.println(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
        label.setText("Bumm");
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (scharf) {
            button.setText("Start");
        } else {
            countdown();
            button.setText("Stopp");
        }
        scharf = !scharf;
    }

    public ZeitbombeBlockiert() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new BorderLayout());
        button.addActionListener(this);
        add(BorderLayout.NORTH, label);
        add(BorderLayout.SOUTH, button);
        setVisible(true);
        pack();
    }
    
    public static void main(String[] args) {
        new ZeitbombeBlockiert();
    }
}
Wenn ein Vorgang länger dauert, kann ein Fenster-Programm nicht mehr auf die Ereignisse reagieren, nicht einmal seine Fenster neu zeichnen. Das muss immer gewährleistet sein.

Eine Lösung besteht darin, längerwährende Aufgaben in einen Thread auszulagern.

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingConstants;

public class ZeitbombeThread extends JFrame implements Runnable, ActionListener {

    JLabel label = new JLabel("Tick", SwingConstants.CENTER);
    JButton button = new JButton("Start");
    Thread ticktack = null;

    @Override
    public void run() {
        for (int i = 10; i > 0; --i) {
            label.setText("" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                return;
            }
        }
        label.setText("Bumm");
        button.setText("Start");
        ticktack = null;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (ticktack != null) {
            ticktack.interrupt();
            ticktack = null;
            button.setText("Start");
        } else {
            ticktack = new Thread(this);
            ticktack.start();
            button.setText("Stopp");
        }
    }

    public ZeitbombeThread() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new BorderLayout());
        button.addActionListener(this);
        add(BorderLayout.NORTH, label);
        add(BorderLayout.SOUTH, button);
        setVisible(true);
        setSize(200, 100);
    }

    public static void main(String[] args) {
        new ZeitbombeThread();
    }
}

Videos


Die Klasse Thread und das Interface Runnable


Synchronisieren von Threads mit join, synchronized, wait-notify und interrupt.


Properties Java