Threads

Wer führt eigentlich welchen Code in einem Java-Programm aus? (Eisenbahnbeispiel 1)

Anlass der Frage war folgender Code:

public class Warum {
    public static void main(String[] argv) {
        Test t = new Test();
        t.start();
        t.dotry();
        t.stop();
    }
}

class Test extends Thread {
    public void run() {
        while (true) {
            System.out.println("Hallo, ich komme.");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // Zum Beenden Enter-Taste druecken
    void dotry() {
        try {
            while (System.in.read() == 0) { }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Ich gehe. Auf Wiedersehen!");
    }
}

Antwort:    Das ist eine sehr interessante Frage. Man muss unterscheiden zwischen dem eigentlichen Thread, der ja eine mehr oder weniger abstrakte Funktionalität des Betriebssystems darstellt, und dem Exemplar der Klasse Thread, welches ja lediglich eine objektorientierte Abstraktion des eigentlichen Threads darstellt, die dazu dient, diesen von Java aus anzusprechen und zu beeinflussen.

In einer idealen OOP-Welt wären die Objekte selbst aktiv, d.h. sie würden prinzipiell parallel agieren und interagieren. Die Realität heutiger OOP-Sprachen sieht aber anders aus: Objekte sind mehr oder weniger passive Gebilde. Sie bündeln Daten mit den auf diesen Daten möglichen Operationen, aber sie sind nicht aktiv. Das Objekt sitzt mit all seinen tollen Methoden herum, und wartet, dass „jemand“ diese ausführt.

Ich verwende gerne folgende (natürlich auch nur begrenzt taugliche) Analogie: Threads sind die Züge, die ein Schienennetz befahren, welches aus den Objekten und ihren Methoden besteht, oder in einer etwas anderen Sichtweise: aus dem Quellcode.

Und auch ein Thread-Exemplar ist ein ganz normales Objekt, mit Methoden wie jedes andere, also ein Teil des Schienennetzes. Dass einige der Methoden dazu führen, dass ein BS-Thread (ein Zug in meinem Bild) gestartet, pausiert oder beendet wird, tut dabei zunächst einmal wenig zur Sache.

Betrachten wir jetzt den fraglichen Quellcode im Lichte dieses Bildes. Zunächst befinden wir uns in der main()-Methode einer Klasse. Das Programm wurde über diese main()-Methode gestartet. Damit haben wir zwangsläufig jemanden, der Code ausführt, also einen Thread, in unserem Bild einen Zug. Den nennen wir mal Z-M (für Main). Z-M fährt nun die main()-Methode ab und kommt zu folgender Zeile:

    Test t = new Test();

Es wird also ein Exemplar der Klasse Test erzeugt, die von der Klasse Thread abgeleitet ist. Damit haben wir ein „stinknormales Objekt“. Ein neuer Zug gelangt hier noch nicht aufs Schienennetz.

    t.start();

Aber nun! Diese spezielle Anweisung erzeugt tatsächlich einen neuen Zug, einen neuen BS-Thread. Nennen wir diesen Z-2. Z-2 wird automatisch auf den Beginn der run()-Methode des von t referenzierten Thread-Exemplars gesetzt und fährt, sobald der Scheduler ihm Rechenzeit gibt, unabhängig von Z-M los und arbeitet den Code der run()-Methode ab. In unserem Falle heißt das, dass er fröhlich im Kreise fährt. Aber was macht in der Zwischenzeit Z-M? Der fährt ja weiter, nämlich zu dieser Zeile:

    t.dotry();

Und hier wird eine Methode eines Objekts aufgerufen. Dass dieses vom Typ Thread ist, ist dabei völlig egal: Z-M befährt die Methode dotry() des von t referenzierten Objekts:

    try {
        while (System.in.read() == 0) { }
    } catch (Exception e) {
        e.printStackTrace();
    }

Das hier ist nicht sonderlich schön... der Vergleich ist, ebenso wie die Schleife im Grunde überflüssig, weil die read()-Methode ohnehin so lange blockiert, bis an der Konsole jemand die Return-Taste drückt. Ein einfaches:

    try {
        System.in.read();
    } catch (Exception e) {
        e.printStackTrace();
    }

hätte es also auch getan. Aber wie auch immer... die read()-Anweisung funktioniert wie ein Stopp-Signal: Unser Z-M-Zug steht so lange, bis eine Zeile von der Konsole gelesen werden kann, also Return gedrückt wurde. Währenddessen befährt Z-2 fröhlich die Schleife in der run-Methode des ihn repräsentierenden Thread-Exemplars.

Wenn nun Return gedrückt wird, fährt Z-M wieder los (sobald er vom Scheduler Rechenzeit bekommt), es wird die Abschiedszeile ausgegeben, und Z-M kehrt zu der Stelle zurück, wo er in die Methode dotry() des Thread-Exemplars abgebogen ist, und gelangt nun zu dieser Zeile:

    t.stop();

Das heißt, Z-M biegt in die stop()-Methode des von t referenzierten Objekts ein. Auch hier ist für Z-M wieder egal, dass es sich bei dem Objekt um eins vom Typ Thread handelt, er sieht stop() als ganz normales Stück Schiene an.

Aber das von t referenzierte Objekt ist halt doch etwas Spezielles, denn es repräsentiert einen BS-Thread, nämlich Z-2. Und seine Methode stop() hat genau den Zweck, dafür zu sorgen, dass dieser BS-Thread Z-2 beendet wird. Was dann auch geschieht. *PLOPP*. Der Zug Z-2 löst sich in Wohlgefallen auf. Das von t referenzierte Objekt allerdings existiert übrigens fröhlich weiter, wie es ja auch schon existierte, als Z-2 noch gar nicht in der Welt war.

Nach Beendigung der stop()-Methode kehrt Z-M wieder zur Aufrufstelle zurück. Aber dort ist die main()-Methode zu Ende... keine Schienen mehr. Und damit löst sich auch Z-M in Wohlgefallen auf. Und da Z-M der letzte Thread war, beendet sich die VM und das Programm ist zu Ende.




Wie funktioniert „synchronized“? (Eisenbahnbeispiel 2)

Antwort:    Stellen wir uns vor, wir haben wieder unsere Thread-Züge, die fröhlich irgendwelche Methoden befahren. Bestimmte Codebereiche sind nun mit Sperrsignalen versehen. Fährt ein Zug in einen solchen Bereich ein, wird hinter ihm das Signal auf rot gestellt. Verlässt der Zug am anderen Ende den Bereich, für den das Signal zuständig ist, wird das Signal wieder auf grün gestellt, z.B. per Funk.

Dieser Mechanismus würde genügen, um zu gewährleisten, dass immer nur ein Zug einen so gesicherten Bereich befährt. Aber wir wollen mehr:

Wir wollen nämlich auch erreichen können, dass in bestimmten verschiedenen Streckenabschnitten immer nur insgesamt ein Zug unterwegs ist. Die Notwendigkeit hierfür ergibt sich daraus, dass die betreffenden Gleisabschnitte nicht wirklich unabhängig voneinander sind. Wir können uns z.B. vorstellen, dass es Kreuzungsstellen gibt. D.h., wenn ein Zug in den Abschnitt A einfährt, muss nicht nur das Signal für Abschnitt A auf rot gehen, sondern auch das für einen weiteren Abschnitt B (und umgekehrt). Und erst, wenn der Zug den Abschnitt A verlässt, sollen beide Signale wieder grün werden.

Das wird nun - weil zu komplex - nicht mehr über direkte Verschaltungen zwischen Gleiskontakten und Signalen realisiert. Stattdessen kennt jeder Streckenabschnitt eine spezielle Funk-Kontrollstation, mit der seine Einfahrtsignale gekoppelt sind, nämlich so, dass die Kontrollstation zwei Zustände rot und grün hat, und die Signale des Streckenabschnittes sich nach dem Zustand der Kontrollstation richten. Der Trick ist nun, dass diese Kontrollstation für mehrere unabhängige Streckenabschnitte dieselbe sein kann! Wenn nun ein Zug in Abschnitt A einfährt, welchem die Kontrollstation X zugeordnet ist, dann wird die Kontrollstation in den Zustand „rot“ geschaltet. Und damit wird dann nicht nur das Einfahrtsignal für Streckenabschnitt A rot, sondern auch das für alle anderen Streckenabschnitte, die ebenfalls an die Kontrollstation X gekoppelt sind.

Die oben postulierten Kreuzungspunkte verschiedener Gleise entsprechen Ressourcen, die in einen inkonsistenten Zustand geraten könnten, wenn mehrere Threads parallel auf sie zugreifen. Und die Kontrollstationen entsprechen natürlich den Objekten, die zur Synchronisation verwendet werden. Bei einer als synchronized gekennzeichneten Methode ist das „this“, bei Blöcken das explizit (über eine Referenz) angegebene Objekt. Die Rolle einer Kontrollstation kann dabei jedes Objekt übernehmen. Ein direkter Zusammenhang zwischen dem zur Synchronisation verwendeten Objekt und irgendwelchen Objekten oder Variablen auf die die synchronisierten Codeabschnitte zugreifen, besteht dabei nicht. Oft ist es natürlich naheliegend, genau die gemeinsam verwendete Ressource, auf die man den Zugriff regeln will, auch als „Kontrollstation“ zu verwenden, denn mit ihr hat man immerhin schon ein Objekt, auf das alle betroffenen Codeabschnitte sowieso schon eine Referenz haben. Aber jede solche Konstruktion lässt sich durch eine ersetzen, bei der stattdessen ein anderes - meist extra erzeugtes - Objekt als „Kontrollstation“ verwendet wird.

Als Kontrollstation kommen übrigens nur Objekte in Frage, keine Primitivtypen. Wenn man also konkurrierende Zugriffe auf eine Primitivtyp-Variable ausschließen will, dann muss man für alle Codeblöcke, welche einen solchen Zugriff durchführen können, ein Objekt finden oder eigens erzeugen, welches für sie als „Kontrollstation“ dient. Dazu muss es natürlich an der Stelle, an der mit Hilfe dieses Objekts synchronisiert werden soll, möglich sein, eine Referenz auf das Objekt zu erhalten.

Wie erklärt das „Eisenbahnbeispiel 2“, dass ein Thread einen Monitor auch mehrfach sperren kann?

Der Sinn der Erlaubnis der mehrfachen Sperrung eines Monitors ist ja, zu ermöglichen, dass ein Thread aus einem Codebereich, der über ein bestimmtes Objekt synchronisiert wurde, weitere Codebereiche betreten kann, die über dasselbe Objekt synchronisiert wurden. Ein typisches Beispiel wäre ein Objekt, dessen sämtliche Methoden als synchronized gekennzeichnet sind: Gäbe es nicht die Möglichkeit der Mehrfachsperrung, dann wäre es nicht möglich, aus einer solchen Methode ein andere Methode desselben Objekts aufzurufen.

Wir hatten ja gesagt, dass durch das Einfahren eines Zuges in einen Abschnitt, welchem eine bestimmte Kontrollstation zugeordnet ist, die Einfahrtsignale für alle Streckenabschnitte, welche über diese Kontrollstation überwacht sind, auf rot geschaltet werden. Damit könnte besagter Zug aber auch nicht in einen anderen Bereich einfahren, der ebenfalls durch die Kontrollstation kontrolliert wird, die er selbst gesperrt hat, obwohl dieser Vorgang ja völlig unproblematisch wäre. D.h., wir müssen das Modell ergänzen:

Um die mögliche Mehrfachsperrung in das Eisenbahnmodell einzubauen, müssen wir zusätzlich postulieren, dass ein Rotsignal für genau einen Zug nicht gilt, nämlich für den, dessen Einfahrt die Rot-Schaltung überhaupt erst ausgelöst hat. Dieser Zug darf, nachdem er einmal in den von einer bestimmten Kontrollstation überwachten Bereich eingefahren ist, alle Rotsignale ignorieren, die zu eben dieser Kontrollstation gehören.

„Schützt“ Synchronisation das Objekt, auf dem synchronisiert wird vor parallelen Zugriffen?

Das kann so sein. Nämlich dann, wenn alle Methoden eines Objekts, die es erlauben, dessen Attribute zu ändern, als synchronized markiert sind und ein Zugriff auf Attribute auch nur über diese Methoden möglich ist. Aber allgemein ist dies nicht der Fall.

Nehmen wir an, der Zugriff auf eine Ressource X ist nur über bestimmte bekannte Codeblöcke möglich. Dann kann man verhindern, dass konkurrierender Zugriff auf X erfolgt, indem man all diese Codeblöcke synchronisiert und dabei zur Synchronisation immer dasselbe Objekt verwendet. Das kann die Ressource X selbst sein, muss es aber nicht.