Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger. 
Inhaltsverzeichnis
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Der Umgang mit Zeichenketten
5 Eigene Klassen schreiben
6 Objektorientierte Beziehungsfragen
7 Ausnahmen müssen sein
8 Äußere.innere Klassen
9 Besondere Typen der Java SE
10 Generics<T>
11 Lambda-Ausdrücke und funktionale Programmierung
12 Architektur, Design und angewandte Objektorientierung
13 Komponenten, JavaBeans und Module
14 Die Klassenbibliothek
15 Einführung in die nebenläufige Programmierung
16 Einführung in Datenstrukturen und Algorithmen
17 Einführung in grafische Oberflächen
18 Einführung in Dateien und Datenströme
19 Einführung ins Datenbankmanagement mit JDBC
20 Einführung in <XML>
21 Testen mit JUnit
22 Bits und Bytes und Mathematisches
23 Die Werkzeuge des JDK
A Java SE-Paketübersicht
Stichwortverzeichnis


Download:

- Beispielprogramme, ca. 35,4 MB


Buch bestellen
Ihre Meinung?



Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom

Einführung, Ausbildung, Praxis
Buch: Java ist auch eine Insel


Java ist auch eine Insel

Pfeil 15 Einführung in die nebenläufige Programmierung
Pfeil 15.1 Nebenläufigkeit und Parallelität
Pfeil 15.1.1 Multitasking, Prozesse, Threads
Pfeil 15.1.2 Threads und Prozesse
Pfeil 15.1.3 Wie nebenläufige Programme die Geschwindigkeit steigern können
Pfeil 15.1.4 Was Java für Nebenläufigkeit alles bietet
Pfeil 15.2 Threads erzeugen
Pfeil 15.2.1 Threads über die Schnittstelle Runnable implementieren
Pfeil 15.2.2 Thread mit Runnable starten
Pfeil 15.2.3 Die Klasse Thread erweitern
Pfeil 15.3 Thread-Eigenschaften und Zustände
Pfeil 15.3.1 Der Name eines Threads
Pfeil 15.3.2 Wer bin ich?
Pfeil 15.3.3 Schläfer gesucht
Pfeil 15.3.4 Mit yield() auf Rechenzeit verzichten
Pfeil 15.3.5 Der Thread als Dämon
Pfeil 15.3.6 Freiheit für den Thread – das Ende
Pfeil 15.3.7 Einen Thread höflich mit Interrupt beenden
Pfeil 15.3.8 UncaughtExceptionHandler für unbehandelte Ausnahmen
Pfeil 15.4 Der Ausführer (Executor) kommt
Pfeil 15.4.1 Die Schnittstelle Executor
Pfeil 15.4.2 Glücklich in der Gruppe – die Thread-Pools
Pfeil 15.4.3 Threads mit Rückgabe über Callable
Pfeil 15.4.4 Mehrere Callables abarbeiten
Pfeil 15.4.5 ScheduledExecutorService für wiederholende Ausgaben und Zeitsteuerungen nutzen
Pfeil 15.5 Synchronisation über kritische Abschnitte
Pfeil 15.5.1 Gemeinsam genutzte Daten
Pfeil 15.5.2 Probleme beim gemeinsamen Zugriff und kritische Abschnitte
Pfeil 15.5.3 Punkte nebenläufig initialisieren
Pfeil 15.5.4 Kritische Abschnitte schützen
Pfeil 15.5.5 Kritische Abschnitte mit ReentrantLock schützen
Pfeil 15.5.6 Synchronisieren mit synchronized
Pfeil 15.5.7 Mit synchronized synchronisierte Blöcke
Pfeil 15.5.8 Dann machen wir doch gleich alles synchronisiert!
Pfeil 15.5.9 Lock-Freigabe im Fall von Exceptions
Pfeil 15.5.10 Deadlocks
Pfeil 15.6 Zum Weiterlesen
 

Zum Seitenanfang

15.3Thread-Eigenschaften und Zustände Zur vorigen ÜberschriftZur nächsten Überschrift

Ein Thread hat eine ganze Reihe von Zuständen, wie einen Namen und eine Priorität, die sich erfragen und setzen lassen. Nicht jede Eigenschaft ist nach dem Start änderbar, doch welche das sind, zeigen die folgenden Abschnitte.

 

Zum Seitenanfang

15.3.1Der Name eines Threads Zur vorigen ÜberschriftZur nächsten Überschrift

Ein Thread hat eine ganze Menge Eigenschaften – wie einen Zustand, eine Priorität und auch einen Namen. Dieser kann mit setName(…) gesetzt und mit getName() erfragt werden:

class java.lang.Thread

implements Runnable
  • Thread(String name)

    Erzeugt ein neues Thread-Objekt und setzt den Namen. Sinnvoll bei Unterklassen, die den Konstruktor über super(name) aufrufen.

  • Thread(Runnable target, String name)

    Erzeugt ein neues Thread-Objekt mit einem Runnable und setzt den Namen.

  • final String getName()

    Liefert den Namen des Threads. Der Name wird im Konstruktor angegeben oder mit setName(…) zugewiesen. Standardmäßig ist der Name »Thread-x«, wobei x eine eindeutige Nummer ist.

  • final void setName(String name)

    Ändert den Namen des Threads.

 

Zum Seitenanfang

15.3.2Wer bin ich? Zur vorigen ÜberschriftZur nächsten Überschrift

Eine Erweiterung der Klasse Thread hat den Vorteil, dass geerbte Methoden wie getName() sofort genutzt werden können. Wenn wir Runnable implementieren, genießen wir diesen Vorteil nicht.

Die Klasse Thread liefert mit der statischen Methode currentThread() die Objektreferenz für das Thread-Exemplar, das diese Anweisung gerade ausführt. Auf diese Weise lassen sich nichtstatische Thread-Methoden wie getName() verwenden.

[zB]Beispiel

Gib die aktuelle Priorität des laufenden Threads und den Namen aus:

System.out.println( Thread.currentThread().getPriority() ); // z. B. 5

System.out.println( Thread.currentThread().getName() ); // z. B. main

Falls es in einer Schleife wiederholten Zugriff auf Thread.currentThread() gibt, sollte das Ergebnis zwischengespeichert werden, denn der Aufruf ist nicht ganz billig.

class java.lang.Thread

implements Runnable
  • static Thread currentThread()

    Liefert den Thread, der das laufende Programmstück ausführt.

 

Zum Seitenanfang

15.3.3Schläfer gesucht Zur vorigen ÜberschriftZur nächsten Überschrift

Manchmal ist es notwendig, einen Thread eine bestimmte Zeit lang anzuhalten. Dazu lassen sich Methoden zweier Klassen nutzen:

  • Die überladene statische Methode Thread.sleep(…): Etwas erstaunlich ist sicherlich, dass sie keine Objektmethode von einem Thread-Objekt ist, sondern eine statische Methode. Ein Grund wäre, dass dadurch verhindert wird, externe Threads zu beeinflussen. Es ist nicht möglich, einen fremden Thread, über dessen Referenz wir verfügen, einfach einige Sekunden lang schlafen zu legen und ihn so von der Ausführung abzuhalten.

  • Die Objektmethode sleep(…) auf einem TimeUnit-Objekt: Auch sie bezieht sich immer auf den ausführenden Thread. Der Vorteil gegenüber sleep(…) ist, dass hier die Zeiteinheiten besser sichtbar sind.

[zB]Beispiel

Der ausführende Thread soll 2 Sekunden lang schlafen. Einmal mit Thread.sleep(…):

try {

Thread.sleep( 2000 );

} catch ( InterruptedException e ) { }

Dann mit TimeUnit:

try {

TimeUnit.SECONDS.sleep( 2 );

} catch ( InterruptedException e ) { }

Der Schlaf kann durch eine InterruptedException unterbrochen werden, etwa durch interrupt(). Die Ausnahme muss behandelt werden, da sie keine RuntimeException ist.

Praktisch wird das Erweitern der Klasse Thread bei inneren anonymen Klassen. Die folgende Anweisung gibt nach 2 Sekunden Schlafzeit eine Meldung auf dem Bildschirm aus:

Listing 15.6com/tutego/insel/thread/SleepInInnerClass.java, main()

new Thread() {

@Override public void run() {

try {

Thread.sleep( 2000 );

System.out.println( "Zeit ist um." );

} catch ( InterruptedException e ) { e.printStackTrace(); }

}

}.start();

Da new Thread(){…} ein Exemplar der anonymen Unterklasse ergibt, lässt die auf dem Ausdruck aufgerufene Objektmethode start() den Thread gleich loslaufen. Aufgaben dieser Art, also etwas nach einer gewissen Zeit zu tun, lösen auch die Timer gut.

class java.lang.Thread

implements Runnable
  • static void sleep(long millis) throws InterruptedException

    Der aktuell ausgeführte Thread wird mindestens millis Millisekunden schlafen gelegt. Unterbricht ein anderer Thread den schlafenden, wird vorzeitig eine InterruptedException ausgelöst.

  • static void sleep(long millis, int nanos) throws InterruptedException

    Der aktuell ausgeführte Thread wird mindestens millis Millisekunden und zusätzlich nanos Nanosekunden schlafen gelegt. Im Gegensatz zu sleep(long) wird bei einer negativen Millisekundenanzahl eine IllegalArgumentException ausgelöst; diese Exception wird auch ausgelöst, wenn die Nanosekundenanzahl nicht zwischen 0 und 999.999 liegt.

enum java.util.concurrent.TimeUnit

extends Enum<TimeUnit>

implements Serializable, Comparable<TimeUnit>
  • NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS

    Aufzählungselemente von TimeUnit

  • void sleep(long timeout) throws InterruptedException

    Führt ein Thread.sleep() für die Zeiteinheit aus.

Eine überladene Methode Thread.sleep(long, TimeUnit) wäre nett, gibt es aber nicht.

UML-Diagramme für Timer und TimerTask

Abbildung 15.4UML-Diagramme für Timer und TimerTask

 

Zum Seitenanfang

15.3.4Mit yield() auf Rechenzeit verzichten Zur vorigen ÜberschriftZur nächsten Überschrift

Neben sleep() gibt es eine weitere Methode, kooperative Threads zu programmieren: die Methode yield(). Sie funktioniert etwas anders als sleep(…), da hier nicht nach Ablauf der genannten Millisekunden zum Thread zurückgekehrt wird, sondern yield() den Thread bezüglich seiner Priorität wieder in die Thread-Warteschlange des Systems einordnet. Einfach ausgedrückt, sagt yield() der Thread-Verwaltung: »Ich setze diese Runde aus und mache weiter, wenn ich das nächste Mal dran bin.«

class java.lang.Thread

implements Runnable
  • static void yield()

    Der laufende Thread gibt freiwillig seine Rechenzeit ab. Die Methode ist für Implementierungen der JVM nicht verbindlich.

 

Zum Seitenanfang

15.3.5Der Thread als Dämon Zur vorigen ÜberschriftZur nächsten Überschrift

Ein Server reagiert oft in einer Endlosschleife auf eingehende Aufträge vom Netzwerk und führt die gewünschte Aufgabe aus. In unseren bisherigen Programmen haben wir oft Endlosschleifen eingesetzt, sodass ein gestarteter Thread nie beendet wird. Wenn also run() wie in den vorangehenden Beispielen nie abbricht (Informatiker sprechen hier von terminiert), läuft der Thread immer weiter, auch wenn die Hauptapplikation beendet ist. Dies ist nicht immer beabsichtigt, da vielleicht Server-Funktionalität nach dem Beenden der Applikation nicht mehr gefragt ist. Dann sollte auch der endlos laufende Thread beendet werden. Um dies auszudrücken, erhält ein im Hintergrund arbeitender Thread eine spezielle Kennung: Der Thread wird als Dämon[ 230 ](Das griechische δαιμων (engl. daemon) bezeichnet allerlei Wesen zwischen Gott und Teufel. Eine gute Einleitung gibt https://de.wikipedia.org/wiki/D%C3%A4mon. ) gekennzeichnet. Standardmäßig ist ein aufgebauter Thread kein Dämon.

Ein Dämon ist wie ein Heinzelmännchen im Hintergrund mit einer Aufgabe beschäftigt. Wenn das Hauptprogramm beendet ist und die Laufzeitumgebung erkennt, dass kein normaler Thread mehr läuft, sondern nur Dämonen, dann ist das Ende der Dämonen eingeläutet, und die JVM kommt zum Ende. Denn Dämonen-Threads sind Zulieferer: Gibt es keine Klienten mehr, werden auch sie nicht mehr gebraucht. Das ist wie bei den Göttern der Scheibenwelt: Glaubt keiner an sie, hören sie auf zu existieren. Wir müssen uns also um das Ende des Dämons nicht kümmern. Gleichzeitig heißt das aber auch, dass ein Dämonen-Thread vorsichtig mit Ein-/Ausgabeoperationen sein muss, denn er kann jederzeit – auch etwa während einer Schreiboperation auf die Festplatte – abgebrochen werden, was zu beschädigten Daten führen kann.

[»]Hinweis

Die automatische Speicherbereinigung ist ein gutes Beispiel für einen Dämon. Nur, wenn es andere Threads gibt, muss der Speicher aufgeräumt werden. Gibt es keine anderen Threads mehr, kann auch die JVM mit beendet werden, was auch die Dämonen-Threads beendet.

Wie ein Thread in Java zum Dämon wird

Einen Thread in Java als Dämon zu kennzeichnen, heißt, die Methode setDaemon(boolean) mit dem Argument true aufzurufen. Die Methode ist nur vor dem Starten des Threads erlaubt. Danach kann der Status nicht wieder vom Dämon in den normalen Benutzer-Thread umgesetzt werden. Die Auswirkungen von setDaemon(true) können wir am folgenden Programm ablesen:

Listing 15.7com/tutego/insel/thread/DaemonThread.java

package com.tutego.insel.thread;



class DaemonThread extends Thread {



DaemonThread() {

setDaemon( true );

}



@Override public void run() {

while ( true )

System.out.println( "Lauf, Thread, lauf" );

}



public static void main( String[] args ) {

new DaemonThread().start();

}

}

In diesem Programm wird der Thread gestartet, und danach ist die Anwendung sofort beendet. Vor dem Ende kann der neue Thread aber schon einige Zeilen auf der Konsole ausgeben. Klammern wir die Anweisung mit setDaemon(true) aus, läuft das Programm ewig, da die Laufzeitumgebung auf das natürliche Ende der Thread-Aktivität wartet.

class java.lang.Thread

implements Runnable
  • final void setDaemon(boolean on)

    Markiert den Thread als Dämon oder normalen Thread. Die Methode muss aufgerufen werden, bevor der Thread gestartet wurde, andernfalls folgt eine IllegalThreadStateException. Mit anderen Worten: Nachträglich kann ein existierender Thread nicht mehr zu einem Dämon gemacht werden, und ihm kann auch nicht die Dämonenhaftigkeit genommen werden, so er sie hat.

  • final boolean isDaemon()

    Testet, ob der Thread ein Dämon-Thread ist.

 

Zum Seitenanfang

15.3.6Freiheit für den Thread – das Ende Zur vorigen ÜberschriftZur nächsten Überschrift

Es gibt Threads, die dauernd laufen, weil sie zum Beispiel Serverfunktionen implementieren. Andere Threads führen einmalig eine Operation aus und sind danach beendet. Allgemein ist ein Thread beendet, wenn eine der folgenden Bedingungen zutrifft:

  • Die run()-Methode wurde ohne Fehler beendet. Wenn wir eine Endlosschleife programmieren, würde diese potenziell einen nie endenden Thread bilden.

  • In der run()-Methode tritt eine RuntimeException auf, die die Methode beendet. Das beendet weder die anderen Threads noch die JVM als Ganzes.

  • Der Thread wurde von außen abgebrochen. Dazu dient die prinzipbedingt problematische Methode stop(), von deren Verwendung abgeraten wird und die auch veraltet ist.

  • Die virtuelle Maschine wird beendet und nimmt alle Threads mit ins Grab.

Wenn der Thread einen Fehler melden soll

Da ein Thread nebenläufig arbeitet, kann die run()-Methode synchron schlecht Exceptions melden oder einen Rückgabewert liefern. Wer sollte auch an welcher Stelle darauf hören? Eine Lösung für das Problem ist ein Listener, der sich beim Thread anmeldet und darüber informiert wird, ob der Thread seine Arbeit machen konnte oder nicht. Eine andere Lösung gibt Callable, mit dem ein spezieller Fehlercode zurückgegeben oder eine Exception angezeigt werden kann. Speziell bei ungeprüften Ausnahmen kann ein UncaughtExceptionHandler weiterhelfen.

 

Zum Seitenanfang

15.3.7Einen Thread höflich mit Interrupt beenden Zur vorigen ÜberschriftZur nächsten Überschrift

Der Thread ist in der Regel zu Ende, wenn die run()-Methode ordentlich bis zum Ende ausgeführt wurde. Enthält eine run()-Methode jedoch eine Endlosschleife – wie etwa bei einem Server, der auf eingehende Anfragen wartet –, so muss der Thread von außen zur Kapitulation gezwungen werden.

Wenn wir den Thread schon nicht von außen beenden wollen, können wir ihn immerhin bitten, seine Arbeit aufzugeben und freiwillig den Freitod zu wählen. Periodisch müsste er dann nur überprüfen, ob jemand von außen den Abbruchswunsch geäußert hat.

Die Methoden interrupt() und isInterrupted()

Die Methode interrupt() setzt von außen in einem Thread-Objekt ein internes Flag, das dann in der run()-Methode durch isInterrupted() periodisch abgefragt werden kann.

Das folgende Programm soll jede halbe Sekunde eine Meldung auf dem Bildschirm ausgeben. Nach 2 Sekunden wird der Unterbrechungswunsch mit interrupt() gemeldet. Auf dieses Signal achtet die sonst unendlich laufende Schleife und bricht ab:

Listing 15.8com/tutego/insel/thread/ThreadusInterruptus.java, main()

Thread t = new Thread() {

@Override

public void run() {

System.out.println( "Es gibt ein Leben vor dem Tod. " );



while ( ! isInterrupted() ) {

System.out.println( "Und er läuft und er läuft und er läuft" );



try {

Thread.sleep( 500 );

}

catch ( InterruptedException e ) {

interrupt();

System.out.println( "Unterbrechung in sleep()" );

}

}



System.out.println( "Das Ende" );

}

};

t.start();

Thread.sleep( 2000 );

t.interrupt();

Die Ausgabe zeigt hübsch die Ablaufsequenz:

Es gibt ein Leben vor dem Tod.

Und er läuft und er läuft und er läuft

Und er läuft und er läuft und er läuft

Und er läuft und er läuft und er läuft

Und er läuft und er läuft und er läuft

Unterbrechung in sleep()

Das Ende

Die run()-Methode im Thread ist so implementiert, dass die Schleife genau dann verlassen wird, wenn isInterrupted() den Wert true ergibt, also von außen die interrupt()-Methode für dieses Thread-Exemplar aufgerufen wurde. Genau dies geschieht in der main(…)-Methode. Auf den ersten Blick ist das Programm leicht verständlich, doch vermutlich erzeugt das interrupt() im catch-Block die Aufmerksamkeit. Stünde diese Zeile dort nicht, würde das Programm aller Wahrscheinlichkeit nach nicht funktionieren. Das Geheimnis ist folgendes: Wenn die Ausgabe nur jede halbe Sekunde stattfindet, befindet sich der Thread fast die gesamte Zeit über in der Schlafmethode sleep(…). Also wird vermutlich der interrupt() den Thread gerade beim Schlafen stören. Genau dann wird sleep(…) durch InterruptedException unterbrochen, und der catch-Behandler fängt die Ausnahme ein. Jetzt passiert aber etwas Unerwartetes: Durch die Unterbrechung wird das interne Flag zurückgesetzt, sodass isInterrupted() meint, die Unterbrechung habe gar nicht stattgefunden. Daher muss interrupt() erneut aufgerufen werden, da das Abbruch-Flag neu gesetzt werden muss, damit isInterrupted() das Ende bestimmen kann.

Wenn wir mit der Objektmethode isInterrupted() arbeiten, müssen wir beachten, dass neben sleep(…) auch die Object-Methoden join(…) und wait(…) durch die InterruptedException das Flag löschen.

[»]Hinweis

Die Methoden sleep(…), wait(…) und join(…) lösen alle eine InterruptedException aus, wenn sie durch die Methode interrupt() unterbrochen werden. Das heißt, interrupt() beendet diese Methoden mit der Ausnahme.

Zusammenfassung: interrupted(), isInterrupted() und interrupt()

Die Methodennamen sind verwirrend gewählt, sodass wir die Aufgaben noch einmal zusammenfassen wollen: Die Objektmethode interrupt() setzt in einem (anderen) Thread-Objekt ein Flag, dass es einen Antrag gab, den Thread zu beenden. Sie beendet aber den Thread nicht, obwohl es der Methodenname nahelegt. Dieses Flag lässt sich mit der Objektmethode isInterrupted() abfragen. In der Regel wird dies innerhalb einer Schleife geschehen, die darüber bestimmt, ob die Aktivität des Threads fortgesetzt werden soll. Die statische Methode interrupted() ist zwar auch eine Anfragemethode und testet das entsprechende Flag des aktuell laufenden Threads, wie Thread.currentThread().isInterrupted(), aber zusätzlich löscht es den Interrupt-Status auch, was isInterrupted() nicht tut. Zwei aufeinanderfolgende Aufrufe von interrupted() führen daher zu einem false, es sei denn, in der Zwischenzeit erfolgt eine weitere Unterbrechung.

 

Zum Seitenanfang

15.3.8UncaughtExceptionHandler für unbehandelte Ausnahmen Zur vorigen ÜberschriftZur nächsten Überschrift

Einer der Gründe für das Ende eines Threads ist eine unbehandelte Ausnahme, etwa von einer nicht aufgefangenen RuntimeException. Um in diesem Fall einen kontrollierten Abgang zu ermöglichen, lässt sich an den Thread ein UncaughtExceptionHandler hängen, der immer dann benachrichtigt wird, wenn der Thread wegen einer nicht behandelten Ausnahme endet.

UncaughtExceptionHandler ist eine in Thread deklarierte innere Schnittstelle, die eine Operation void uncaughtException(Thread t, Throwable e) vorschreibt. Eine Implementierung der Schnittstelle lässt sich entweder einem individuellen Thread oder allen Threads anhängen, sodass im Fall des Abbruchs durch unbehandelte Ausnahmen die JVM die Methode uncaughtException(…) aufruft. Auf diese Weise kann die Applikation im letzten Atemzug noch den Fehler loggen, den die JVM über das Throwable e übergibt.

class java.lang.Thread

implements Runnable
  • void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

    Setzt den UncaughtExceptionHandler für den Thread.

  • Thread.UncaughtExceptionHandler getUncaughtExceptionHandler()

    Liefert den aktuellen UncaughtExceptionHandler.

  • static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

    Setzt den UncaughtExceptionHandler für alle Threads.

  • static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()

    Liefert den zugewiesenen UncaughtExceptionHandler aller Threads.

Ein mit setUncaughtExceptionHandler(…) lokal gesetzter UncaughtExceptionHandler überschreibt den Eintrag für den setDefaultUncaughtExceptionHandler(…). Zwischen dem mit dem Thread assoziierten Handler und dem globalen gibt es noch einen Handler-Typ für Thread-Gruppen, der jedoch seltener verwendet wird.

 


Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück

 

 


Copyright © Rheinwerk Verlag GmbH 2017

Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 

[Rheinwerk Computing]



Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de