Open Source im professionellen Einsatz
Linux-Magazin 09/2012
© nena2112, photocase.com

© nena2112, photocase.com

Thread-Programmierung mit Java

Eingefädelt

Seit der ersten Version von Java sind Threads ein fester Bestandteil der Sprache. Das macht vieles einfacher als in anderen Programmiersprachen. Neuere Versionen der Java-Bibliothek bieten darüber hinaus viele nützliche Klassen für Locking und Synchronisierung.

832

Als die erste Java-Version im Jahr 1995 erschien, gab es kaum einen Desktop-Rechner mit mehr als einem Prozessor. Trotzdem war die Unterstützung von Threads von Anfang an ein fester Bestandteil der Sprache und für den zentralen Anwendungsfall notwendig. Der Hintergrund war, dass Java als Frontend für Serveranwendungen dienen und das Frontend nicht durch langsame Dateitransfers lahmgelegt werden sollte.

Weitsicht

Die weitere Entwicklung von Java ging dann zwar in eine andere Richtung – Java-Applets spielen kaum mehr eine Rolle –, doch von der Weitsicht der Sprachentwickler profitieren Java-Programmierer nach wie vor.

Die Grundlage aller Threadprogramme sind das Interface »java.lang.Runnable« und die Klasse »java.lang.Thread« . Das Interface definiert als einzige Schnittstelle die »run()« -Methode. Eigene Threads erweitern entweder »Thread« oder implementieren »Runnable« und starten den Thread mit der statischen Methode »Thread.start(Runnable r)« .

Der gesicherte Zugriff auf gemeinsam genutzte Ressourcen gelingt mit Hilfe des »synchronized« -Schlüsselworts. Der Programmierer kann Synchronisierung auf drei Ebenen durchführen. Möglich sind die Synchronisierung der gesamten Klasse (etwa »public synchronized class Foo{...}« ), einzelner Methoden (»public synchronized int getID() {...}« oder einzelner Code-Abschnitte.

Die Beispielklasse in Listing 1 vergibt für jedes neu geschaffene Objekt eine eindeutige ID aus einer globalen Sequenznummer. In diesem trivialen Beispiel ist natürlich die »getID()« -Methode nicht wirklich nötig, es würde reichen, wenn der Konstruktor synchronisiert wäre. Die »main()« -Methode ab Zeile 21 erzeugt 100 Threads mit einer anonymen Runnable-Klasse. Wer das Verhalten ohne »synchronized« testen will, der entfernt dieses Schlüsselwort aus Zeile 9 und führt folgende Zeilen aus:

Listing 1

Schlüsselwort synchronized

01 import java.lang.*;
02 import java.io.*;
03
04 class ObjectID {
05   private static int seq=0;
06
07   private int id;
08
09   private synchronized int getID() {
10     return seq++;
11   }
12
13   public ObjectID() {
14     id = getID();
15   }
16
17   public void print() {
18     System.out.println("ID: " + id);
19   }
20
21   public static void main(String[] args) {
22     for (int i=0; i<100; ++i) {
23       new Thread(new Runnable() {
24         public void run() {
25           try {
26             Thread.currentThread().sleep(100);
27           } catch (Exception e) {
28           }
29           ObjectID o = new ObjectID();
30           o.print();
31         }
32       }).start();
33     }
34   }
35 }
javac -d . ObjectID.java
java -classpath . ObjectID | sort | uniq -c

Nicht bei jedem Lauf kommt es zu Problemen, je nach Geschwindigkeit des Rechners und der vorhandenen Prozessoren fällt es sogar schwer, zwei Objekte mit gleicher ID zu provozieren (Abbildung 1). Dies ist übrigens einer der Gründe, warum Fehler in parallelen Programmen häufig schwer zu finden und zu korrigieren sind.

Abbildung 1: Ohne synchchronized erzeugt Listing 1 gelegentlich zwei Objekte mit der gleichen ID, zu erkennen an der Zwei in der ersten Spalte.

Die Wahl der Synchronisationsebene wirkt sich auf die Effizienz der Programme aus. Je kleiner der Geltungsbereich des Lock ist, desto geringer die Auswirkungen auf andere, eventuell unbeteiligte Programmteile.

Die Java-Sprachdesigner bewiesen zwar Weitsicht mit der Integration des Threadings. Ihnen ist jedoch beim ersten Wurf ein folgenschwerer Fehler unterlaufen. So enthält die Threadklasse mit »Thread.suspend()« , »Thread.resume()« , »Thread.stop()« und »Thread.destroy()« eine Reihe scheinbar sehr nützlicher Funktionen für das Threadmanagement. Dummerweise sind diese Methoden inhärent fehlerhaft und wurden schon in der Java-Version 1.1 als »deprecated« erklärt. Die Dokumentation des JDK enthält dazu sogar ein eigenes Dokument, das die Problematik beschreibt [1].

Die sichere Methode, einen Thread zu stoppen, besteht darin, eine Instanzvariable innerhalb des Thread zu ändern. Die »run()« -Methode muss diese Variable regelmäßig auf Änderungen überwachen. Alternativ ruft der Programmierer die Methode »Thread.interrupt()« auf. Threads, die auf Ein- oder Ausgaben warten, reagieren aber nicht darauf.

Eine weitere Methode der Threadklasse, die je nach Implementierung anders tickt, ist »Thread.setPriority()« . Die Prioritätssteuerung hat nichts mit irgendwelchen Nice-Werten unter Linux zu tun, sondern dient allein dem internen Thread-Dispatcher der JVM. Ob und wie er die einzelnen Prioritäten behandelt, ist Implementationssache.

Java befindet sich nach wie vor in ständiger Weiterentwicklung, die Sprache hat mit jeder Version weitere Features im Multithreading-Bereich gewonnen. In Version 1.2 kam zum Beispiel die Klasse »ThreadLocal« hinzu. Sie erlaubt Klassenvariablen, die unterschiedlich für jeden Thread sind.

Collections

Eine wichtige Erweiterung von Java 2 war zudem das Collection-Framework. Collections sind nicht per se Thread-sicher, besitzen aber so genannte Fail-Fast-Iteratoren. Das bedeutet, dass Iteratoren sofort und sauber eine Exception werfen, wenn ein anderer Thread eine Collection verändert hat, während der Programmierer über die Collection iteriert. Braucht er tatsächlich eine komplett Thread-sichere Collection, dann liefern diese verschiedene Factory-Methoden wie etwa »List.synchronizedList()« .

Mit den Thread-APIs von Java 1.2 hatte der Entwickler alles, was er für Multithreaded-Programme braucht. Trotzdem war das Programmieren eher mühselig, denn es handelte sich um Low-Level-APIs. Wichtige Fragen, die in jedem Programm immer wieder auftauchen, blieben unbeantwortet: Wie sollen Abbrüche behandelt werden? Muss das Programm für jede Aufgabe einen neuen Thread erzeugen? Wie verhindere ich, dass zu viele Threads das System lahmlegen? Wie synchronisieren sich mehrere Threads am sinnvollsten? Wie implementiere ich atomare Operationen?

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 5 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

  • Eingezäumt

    Threads, die Arbeitstiere jeder Qt-Applikation, sorgen für ein reaktives GUI und ein verbessertes Benutzererlebnis - wenn sie die CPU parallel nutzen. Dazu muss der Entwickler sie mit einem Entwurfsmuster erst einmal bändigen, denn andernfalls hören sie nicht auf externe Kommandos.

  • Klon-Debatte

     Die Thread-Behandlung ist mitentscheidend für die Performance und Parallelisierbarkeit von Linux-Anwendungen. Im folgenden Beitrag geht es darum, wie Threads und Prozesse arbeiten und wie die aktuellen Entwicklungen auf diesem Gebiet aussehen.

  • Oracle gibt Java 7 offiziell frei

    Die neueste Version des Java Development Kit ist von Oracle offiziell freigegeben worden.

  • 80 Prozent Java

    Reine Java-Programme laufen auf allen Plattformen. Doch manchmal ist eine engere Verzahnung mit dem jeweiligen Betriebssystem notwendig. Wie das zu bewerkstelligen ist und welche Pakete es dafür in der Open-Source-Landschaft bereits gibt, zeigt dieser Beitrag.

  • Sauber eingefädelt

    Der Standard für Threads unter Linux ist heute die Native Posix Threads Library (NPTL). Die Bibliothek überzeugt durch große Kompatibilität zum Standard und hohe Performance. Dieser Artikel untersucht die neue Threading-Engine und zeigt, wie Benutzeranwendungen davon profitieren.

comments powered by Disqus

Stellenmarkt

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.