Überraschungen bei der nebenläufigen Programmierung in Java

Sprachen  –  0 Kommentare

Java hält seit den Anfängen alles bereit, was für die nebenläufige Programmierung wichtig ist: Threads, ein Memory Model sowie die Schlüsselwörter synchronized und volatile. Um Letzteres ranken sich die meisten Mythen.

In den meisten Java-Projekten wird Nebenläufigkeit nicht gezielt eingebaut, stattdessen ist sie eine Randbedingung, die immer mal wieder zu beachten ist. Ein Beispiel aus dem Projektalltag ist etwa ein automatischer Twitter-Mechanismus, der für einen Kunden an ein großes Content Management System (CMS) angeschlossen werden soll. Dabei sollen die URLs neu erstellter Artikel beim Veröffentlichen immer automatisch mit der zugehörigen Überschrift getwittert werden. Dazu prüft das System innerhalb einer Schleife regelmäßig auf neue Artikel. Findet sich einer, stößt das System eine Reihe automatisierter Prozesse an, unter anderem die Übergabe des Artikels an den Twitter-Service.

Als eine Art Not-Aus soll das Twittern über ein Flag abschaltbar sein. Dieses überprüft das System vor dem Aufruf des Twitter-Services und soll sich über die Java Management Extensions (JMX) schalten lassen. Hier ist die Nebenläufigkeit vielleicht nicht offensichtlich. Allerdings laufen der JMX Connector, der die JMX Bean aufruft, in einem eigenen Thread und die Schleife möglicherweise im Haupt-Thread der Anwendung. Damit sind mindestens zwei Threads an der korrekten Funktion des geschilderten Beispiels beteiligt.

Doch wo liegen die Herausforderungen bei nebenläufigen Programmen? Antworten und weitere Details zu dem Thema finden sich unter anderem im Buch "Java Concurrency in Practice" [1]. Hier soll die Frage nur soweit beantwortet werden, wie es für das weitere Verständnis des Inhalts notwendig ist.

Solange sich innerhalb eines Programms jeder Thread unabhängig von den anderen nur um seine eigenen Aufgaben kümmert, sind keine weiteren Vorkehrungen zu treffen. Vorsicht ist hingegen dann geboten, wenn Threads auf geteilte Ressourcen zugreifen, etwa auf gemeinsame Variablen.

Wenn aber der eine Thread die Nachrichten zu Twitter schickt und der JMX Connector Thread nur sein eigenes isTwitterActive-Flag beschreibt, wäre das geforderte Not-Aus funktionslos. Damit das Ganze funktioniert, brauchen beide Threads das Flag als gemeinsame Variable.

01 class SimpleExample {
02 static class Looper extends Thread {
03 boolean finish = false;
04
05 @Override public void run() {
06 while (!finish) {
07 // do something
08 }
09 }
10 }
11
12 public static void main(String[] args) throws Exception {
13 Looper looper = new Looper();
14 looper.start();
15 Thread.sleep(1000); // wait 1s
16 looper.finish = true;
17 System.out.println("Wait for Looper to terminate...");
18 looper.join();
19 System.out.println("Done.");
20 }
21 }

Die Überraschung

Für die weiteren Untersuchungen reduziert der Artikel das Szenario auf das im Listing gezeigte einfache Beispiel SimpleExample. Dabei wird beim Programmstart zunächst ein neuer Looper-Thread erstellt und gestartet. Er durchläuft nun in der run-Methode seine while-Schleife so lang, bis das Flag finish auf true gesetzt wird. Die main-Methode legt sich nun für eine Sekunde schlafen, setzt anschließend das Flag finish des erzeugten Looper-Threads auf true und wartet mit join() auf dessen Beendigung. Im Anschluss gibt das Programm noch kurz eine Bestätigung aus und terminiert.

Beim Start des Programms würde der Entwickler in der Konsole nun zunächst

$ java SimpleExample
Wait for Looper to terminate...

zu sehen bekommen und nach etwa einer Sekunde die Ausgabe Done. erwarten, worauf sich das Programm beenden sollte. Das tatsächliche Ergebnis ist aber stark vom verwendeten System abhängig. Mit einer aktuellen Java-Version auf einem 64-Bit-System wird das Programm aller Voraussicht nach nicht terminieren. Während die main-Methode auf den Looper-Thread wartet, bleibt dieser endlos in seiner while-Schleife hängen. Auf einem 32-Bit-System mit weniger als 2 GByte Hauptspeicher funktioniert das Programm wahrscheinlich wie gewünscht. Dazu später mehr.

Der nächste Schritt wäre, das Programm zur Fehlersuche im Debugger zu starten. Sitzt der Breakpoint etwa in Zeile 6 im Kopf der while-Schleife, behauptet der Debugger, nachdem er das Programm angehalten hat, dass finish nach der verstrichenen Sekunde tatsächlich den Wert true hat. Lässt man das Programm weiter laufen, wird es wie gewünscht terminieren.

Der nächste Schritt wäre, eine Debug-Ausgabe in die Schleife zu bauen:

06 while (!finish) {
07 System.out.println("finish is " + finish);
08 }

Beim erneuten Start des Programm bekommt man einige Male die Ausgabe finish is false zu sehen, anschließend wird das Programm terminieren. Damit es korrekt funktioniert, auch ohne Debugger und Debug-Ausgabe, ist die Variable finish als volatile zu deklarieren:

volatile boolean finish = false;

volatile stellt sicher, dass Änderungen an der Variable durch den einen Thread auch in anderen Threads ankommen.