next up previous contents
Next: Das Speichermodell Up: Nebenläufigkeit Previous: Nebenläufigkeit   Inhalt

Threads und Synchronisierung in Java

Threads sind in Java das Mittel zum Ausdruck von Nebenläufigkeit. Ein Thread entspricht einem Exemplar der Systemklasse java.lang.Thread oder einer Subklasse. Für jeden Thread führt die virtuelle Maschine nebenläufig die Methode run dieser Klasse aus. Ist die Ausführung der Methode beendet, terminiert der Thread. Threads werden in Java also durch die Sprachmittel Objekterzeugung und Methodenaufruf dargestellt. Nebenläufigkeit durch Threads findet sich in vielen Programmiersprachen [Nelson, 1991,Stoutamire and Omohundro, 1996] oder Bibliotheken [POSIX, 1996,Rogue Wave, 2000] und ist somit ein verbreitetes Programmiermodell.

Abbildung 2.7 zeigt ein Codefragment, das ein neues Thread-Objekt erzeugt und startet.

Abbildung 2.7: Thread-Beispiel
\includegraphics{nebenlaeufigkeit/util/thread-code-example}

Zur Synchronisierung lehnt sich die Java-Spezifikation an das Modell des ,,Monitors`` nach [Hoare, 1974] an. Ein Monitor erfüllt zwei Aufgaben: er erlaubt gegenseitigen Ausschluß und damit geschützten Zugang zu einer Ressource. Weiterhin fungiert ein Monitor als Ereignisvariable2.6, das heißt, durch ihn können Ereignisse zwischen Threads ausgetauscht werden. Während diese Ereignisse selbst keine Information tragen, findet Informationsfluß über den globalen Objektspeicher statt. Dieses Modell belegt die konzeptuelle Verwandtschaft des Java-Modells zu Multiprozessor-Architekturen mit gemeinsamen Speicher und steht im Kontrast zu Architekturen wie PVM [Sunderam, 1989] und MPI [Brunck, 1994], die auf der Technik des message passing basieren.

Gegenseitiger Ausschluß wird in Java durch das Schlüsselwort synchronized in der Methodensignatur oder durch einen ,,synchronized block`` der Form synchronized(expr) { statements } realisiert. In beiden Fällen wird der Monitor des betroffenen Objekts erworben, das heißt entweder des Empfängerobjekts im Falle einer ,,synchronized method`` oder des Ergebnisses des Synchronisierungsausdrucks eines ,,synchronized block``.

Aus obiger Definition folgt, daß Monitore von einem Java-Programm nur in Blockstruktur erworben werden; je zwei Monitore können nur in umgekehrter Erwerbungsreihenfolge wieder freigegeben werden. Auf Bytecode-Ebene ist dies allerdings nicht der Fall: Monitore werden durch Bytecode-Instruktionen monitorenter und monitorexit erworben und freigegeben; dabei ist keine Schachtelungsreihenfolge vorgegeben. Compiler anderer Programmiersprachen können daher legalen Bytecode erzeugen, der andere Freigabereihenfolgen benutzt.

Java Monitore sind rekursiv; sie können von einem Thread beliebig oft erworben werden. Monitore sind blockierend; ein synchronisierender Thread blockiert so lange, bis der Monitor erworben werden konnte. Es gibt keine Möglichkeit, die Verfügbarkeit eines Monitors zu erfragen.

Um die Signalvariable eines Monitors zu nutzen, muß dieser bereits erworben sein. Durch Warten auf der Signalvariablen wird der Monitor freigegeben und der Thread suspendiert. Nun haben andere Threads die Chance, den Monitor zu erwerben, den Objektzustand zu verändern und dies über die Signalvariable zu signalisieren. Ein oder mehrere wartende Threads werden daraufhin fortgesetzt und versuchen, den Monitor wieder zu erwerben, sobald dieser vom signalisierenden Thread freigegeben wurde2.7. Gelingt dies, kehrt der Thread aus dem Warteaufruf zurück und kann fortfahren.

Warten und Signalisieren wird durch Aufruf der Methoden wait und notify[All] der Klasse java.lang.Object realisiert.

Abbildung 2.8: Monitor-Beispiel
\includegraphics{nebenlaeufigkeit/util/sync-code-example}

Das Beispielprogramm in Abbildung 2.8 zeigt den Umgang mit Synchronisierungsoperationen in Java. Die Thread-Zustände sind am Rand markiert und entsprechen denen aus Abbildung 2.9. Dargestellt ist die Definition eines ,,worker threads``. Dieser entfernt wiederholt aus einer Menge von Arbeitsaufträgen einen Job und führt diesen aus. Ist kein Job vorhanden, so wartet der Thread auf der Ereignisvariable des Objekts incoming. Ein Auftraggeber signalisiert diese entsprechend.

Abbildung 2.9: Zustandsdiagramm eines Threads und eines Monitors
\includegraphics{nebenlaeufigkeit/util/sync-state-diagram}

Abbildung 2.9 erläutert die Zustände synct,m , die ein Thread t gegenüber einem Monitor m einnehmen kann. Die möglichen Übergänge sind:

u $\rightarrow$ e
Erwerb eines freien Monitors
u $\rightarrow$ b
versuchter Erwerb eines bereits erworbenen Monitors
b $\rightarrow$ e
Erwerb eines Monitors nach dessen Freigabe
e $\rightarrow$ e
rekursiver Erwerb oder Freigabe eines bereits erworbenen Monitors
e $\rightarrow$ w
Warten auf Signalvariable
w $\rightarrow$ e
Wiedererwerb nach Signalisierung
w $\rightarrow$ b
versuchter Wiedererwerb nach Signalisierung

Man beachte, daß diese Zustände nur in Bezug auf einen Monitor gelten. Ein Thread kann gleichzeitig beliebig viele verschiedene Monitore erworben haben ( $m1\neq{}m2 \wedge{} sync_{t,m1} = sync_{t,m2} = e$), blockieren oder warten kann er jedoch nur auf einem ( $\neg\exists{}m1\neq{}m2: sync_{t,m1} = sync_{t,m2} = b$). Das atomare Erwerben mehrerer Monitore ist nicht vorgesehen.

Versuchen zwei Threads zur gleichen Zeit, den Monitor zu erwerben, zu muß ein Thread blockieren -- ein solcher Konflikt wird als contention bezeichnet.


next up previous contents
Next: Das Speichermodell Up: Nebenläufigkeit Previous: Nebenläufigkeit   Inhalt

2001-02-28