Java und die Synchronisation: Unterschied zwischen den Versionen
(→Beschreibung der allgemeinen Probleme) |
K |
||
(9 dazwischenliegende Versionen von 3 Benutzern werden nicht angezeigt) | |||
Zeile 7: | Zeile 7: | ||
= Einführung = | = Einführung = | ||
− | Die Synchronisation ist ein wesentliches Element, sobald mehrere | + | Die Synchronisation ist ein wesentliches Element, sobald mehrere [[Thread]]s benutzt werden (das heißt in fast allen Anwendungen). In der Tat, ohne Synchronisation ist es unmöglich, eine robuste Anwendung zu entwickeln, die funktioniert, ungeachtet der Verflechtungen ihrer Threads. |
Zeile 18: | Zeile 18: | ||
'''Mehrere Synchronisationsprobleme stellen sich hier:''' | '''Mehrere Synchronisationsprobleme stellen sich hier:''' | ||
− | # Mehrere Empfänger können nicht gleichzeitig eine Arbeit in die Warteschlange der Arbeiten ablegen, andernfalls wären diese Aufträge in demselben Kasten; | + | # Mehrere Empfänger können nicht gleichzeitig eine Arbeit in die [[Warteschlange]] der Arbeiten ablegen, andernfalls wären diese Aufträge in demselben Kasten; |
# Wenn die Warteschlange der Arbeiten voll ist, müssen die Empfänger darauf warten, dass ein Kasten frei wird, um eine neue Arbeit abzulegen; | # Wenn die Warteschlange der Arbeiten voll ist, müssen die Empfänger darauf warten, dass ein Kasten frei wird, um eine neue Arbeit abzulegen; | ||
# Mehrere Arbeiter können nicht jeder gleichzeitig eine Arbeit nehmen, andernfalls hätten sie dieselbe Arbeit zu erledigen; | # Mehrere Arbeiter können nicht jeder gleichzeitig eine Arbeit nehmen, andernfalls hätten sie dieselbe Arbeit zu erledigen; | ||
Zeile 41: | Zeile 41: | ||
Der gegenseitige Ausschluss erlaubt, die Synchronisationsprobleme 1, 3 und 5 aus der Beschreibung der allgemeinen Probleme zu lösen. Hier ein Programmbeispiel, das die Sicherheit nicht respektiert, wenn mehrere Threads dort gleichzeitig Zugang haben : | Der gegenseitige Ausschluss erlaubt, die Synchronisationsprobleme 1, 3 und 5 aus der Beschreibung der allgemeinen Probleme zu lösen. Hier ein Programmbeispiel, das die Sicherheit nicht respektiert, wenn mehrere Threads dort gleichzeitig Zugang haben : | ||
− | < | + | <syntaxhighlight lang="java">class StringList { |
private String[] list = new String[ 50 ]; | private String[] list = new String[ 50 ]; | ||
Zeile 51: | Zeile 51: | ||
} | } | ||
− | }</ | + | }</syntaxhighlight> |
− | Dieses Programm, wie du es geraten haben wirst, erlaubt es, eine Liste von Strings zu verwalten, indem es ein Array benutzt. | + | Dieses [[Programm]], wie du es geraten haben wirst, erlaubt es, eine Liste von Strings zu verwalten, indem es ein [[Array]] benutzt. |
− | < | + | <syntaxhighlight lang="java">void add ( String s ) { void add ( String s ) { |
list[index] = s; //(a1) list[index] = s; //(b1) | list[index] = s; //(a1) list[index] = s; //(b1) | ||
index++; //(a2) index++; //(b2) | index++; //(a2) index++; //(b2) | ||
− | }</ | + | }</syntaxhighlight> |
Zeile 68: | Zeile 68: | ||
*(a1) (b1) (b2) (a2) ist eine mögliche, aber zusammenhanglose Ausführung: die Tabelle enthält die durch T1 hinzugefügte Zeichenfolge nicht, und ein Kasten der Liste ist leer. | *(a1) (b1) (b2) (a2) ist eine mögliche, aber zusammenhanglose Ausführung: die Tabelle enthält die durch T1 hinzugefügte Zeichenfolge nicht, und ein Kasten der Liste ist leer. | ||
− | Mehrere andere Ausführungen können zu anderen Ergebnissen führen, von denen einige zusammenhanglos sind. Dieses rückläufige Problem kann durch den gegenseitigen Ausschluss gelöst werden. Die Funktion add(String) wird '''kritischer Abschnitt''' genannt. Mehrere abhängige kritische Abschnitte dürfen ihren Code nie gleichzeitig ausführen (durch mehrere verschiedene Threads): man sagt, dass sie in gegenseitigem Ausschluss sind. Im vorhergehenden Beispiel ist add(String) in gegenseitigem Ausschluss mit sich selbst, aber man kann sich sicher gut vorstellen, dass sie auch in gegenseitigem Ausschluss mit der Funktion „remove“ ist... | + | Mehrere andere Ausführungen können zu anderen Ergebnissen führen, von denen einige zusammenhanglos sind. Dieses rückläufige Problem kann durch den gegenseitigen Ausschluss gelöst werden. Die Funktion add([[String]]) wird '''kritischer Abschnitt''' genannt. Mehrere abhängige kritische Abschnitte dürfen ihren Code nie gleichzeitig ausführen (durch mehrere verschiedene Threads): man sagt, dass sie in gegenseitigem Ausschluss sind. Im vorhergehenden Beispiel ist add(String) in gegenseitigem Ausschluss mit sich selbst, aber man kann sich sicher gut vorstellen, dass sie auch in gegenseitigem Ausschluss mit der Funktion „remove“ ist... |
Um den gegenseitigen Ausschluss zu schaffen, muss man '''Locks''' (Riegel) benutzen. Wenn ein Thread auf einen kritischen Abschnitt zukommt, verlangt er den Lock. Wenn er ihn erhält, kann er den Code ausführen. Wenn er ihn nicht erhält, weil ein anderer Thread ihn bereits genommen hat, dann wird er blockiert, indem er darauf wartet, ihn zu erhalten. Es ist möglich, eine potentiell unendliche Anzahl von Locks zu benutzen, und demnach präzise gegenseitige Ausschlüsse zu verwirklichen: zum Beispiel muss a() in gegenseitigem Ausschluss mit sich selbst und mit b() sein, während c() in gegenseitigem Ausschluss mit sich selbst und mit d()sein muss, aber nicht mit a() und b()... | Um den gegenseitigen Ausschluss zu schaffen, muss man '''Locks''' (Riegel) benutzen. Wenn ein Thread auf einen kritischen Abschnitt zukommt, verlangt er den Lock. Wenn er ihn erhält, kann er den Code ausführen. Wenn er ihn nicht erhält, weil ein anderer Thread ihn bereits genommen hat, dann wird er blockiert, indem er darauf wartet, ihn zu erhalten. Es ist möglich, eine potentiell unendliche Anzahl von Locks zu benutzen, und demnach präzise gegenseitige Ausschlüsse zu verwirklichen: zum Beispiel muss a() in gegenseitigem Ausschluss mit sich selbst und mit b() sein, während c() in gegenseitigem Ausschluss mit sich selbst und mit d()sein muss, aber nicht mit a() und b()... | ||
Zeile 77: | Zeile 77: | ||
== Das Schlüsselwort synchronized == | == Das Schlüsselwort synchronized == | ||
− | Um dies in Java zu verwirklichen, besteht die einfachste Methode darin, das Schlüsselwort '''synchronized''' zu benutzen. Hier sieht man, wie es benutzt wird: | + | Um dies in [[Java]] zu verwirklichen, besteht die einfachste Methode darin, das Schlüsselwort '''synchronized''' zu benutzen. Hier sieht man, wie es benutzt wird: |
− | < | + | <syntaxhighlight lang="java">synchronized ( einObjekt ) { |
//kritischer Abschnitt | //kritischer Abschnitt | ||
− | }</ | + | }</syntaxhighlight> |
− | „einObjekt“ stellt | + | „einObjekt“ stellt einen Lock (Riegel) dar, welcher irgendein Java Objekt sein kann. Aber Vorsicht, es ist besser, Referenzen zu benutzen, die als „final“ deklariert wurden, um sicher zu sein, dass die Referenz auf das Objekt nicht geändert wird; in der Tat, der nachfolgende Code funktioniert nicht: |
− | < | + | <syntaxhighlight lang="java">synchronized (meineListe) { |
meineListe = new ArrayList<String>() ; | meineListe = new ArrayList<String>() ; | ||
− | }</ | + | }</syntaxhighlight> |
Zeile 95: | Zeile 95: | ||
'''Ein besonderer Fall''': Wenn das Objekt, das als Lock für die Synchronisation dient, '''this''' ist und den ganzen Code einer Methode einschließt, kann man das Schlüsselwort '''synchronized''' auch in den Methodenkopf setzen. Die zwei nachstehenden Codes sind somit absolut gleichwertig: | '''Ein besonderer Fall''': Wenn das Objekt, das als Lock für die Synchronisation dient, '''this''' ist und den ganzen Code einer Methode einschließt, kann man das Schlüsselwort '''synchronized''' auch in den Methodenkopf setzen. Die zwei nachstehenden Codes sind somit absolut gleichwertig: | ||
− | < | + | <syntaxhighlight lang="java">void methode () { |
synchronized ( this ) { | synchronized ( this ) { | ||
//kritischer Abschnitt | //kritischer Abschnitt | ||
} | } | ||
− | }</ | + | }</syntaxhighlight> |
− | < | + | <syntaxhighlight lang="java">synchronized void methode () { |
//kritischer Abschnitt | //kritischer Abschnitt | ||
− | }</ | + | }</syntaxhighlight> |
== Das Paket java.util.concurrent.locks == | == Das Paket java.util.concurrent.locks == | ||
− | Eine andere Methode, um Locks zu verwirklichen, ist erstmals in Java 1.5. erschienen. Sie kann auf den ersten Blick schwieriger scheinen, aber sie ist | + | Eine andere Methode, um Locks zu verwirklichen, ist erstmals in Java 1.5. erschienen. Sie kann auf den ersten Blick schwieriger scheinen, aber sie ist machtvoller, da sie erlaubt, Sachen zu machen, die das Schlüsselwort '''synchronized''' nicht erlaubt (wir werden später darauf zurückkommen). Hier siehst du, wie sie zu benutzen ist: |
− | < | + | <syntaxhighlight lang="java"> Lock l; |
... | ... | ||
l = new ReentrantLock(); | l = new ReentrantLock(); | ||
Zeile 115: | Zeile 115: | ||
l.lock(); | l.lock(); | ||
try { | try { | ||
− | + | //kritischer Abschnitt | |
} finally { | } finally { | ||
l.unlock(); | l.unlock(); | ||
− | }</ | + | }</syntaxhighlight> |
Zeile 125: | Zeile 125: | ||
Jedoch, wenn du einen dermaßen einfachen gegenseitigen Ausschluss machen willst, wie jene der hier vorgestellten Beispiele, rate ich dir '''synchronized''' zu benutzen und die Methode des Pakets java.util.concurrent.locks für recht besondere Fälle zu reservieren, die wir später sehen werden. | Jedoch, wenn du einen dermaßen einfachen gegenseitigen Ausschluss machen willst, wie jene der hier vorgestellten Beispiele, rate ich dir '''synchronized''' zu benutzen und die Methode des Pakets java.util.concurrent.locks für recht besondere Fälle zu reservieren, die wir später sehen werden. | ||
− | |||
= Gemeinsame Synchronisation = | = Gemeinsame Synchronisation = | ||
Zeile 136: | Zeile 135: | ||
=== Benutzung === | === Benutzung === | ||
− | Die grundlegendste Art der Synchronisation zwischen mehreren Java Threads ist die Benutzung der Methoden wait(), notify() und eventuell notifyAll(), die in der Object-Klasse definiert sind. Um ihre Funktionsweise zu begreifen, vervollständigen unser Beispiel von soeben: | + | Die grundlegendste Art der Synchronisation zwischen mehreren Java Threads ist die Benutzung der Methoden wait(), notify() und eventuell notifyAll(), die in der Object-Klasse definiert sind. Um ihre Funktionsweise zu begreifen, vervollständigen wir unser Beispiel von soeben: |
− | < | + | <syntaxhighlight lang="java">class StringList { |
private String [] list = new String [ 50 ] ; | private String [] list = new String [ 50 ] ; | ||
Zeile 163: | Zeile 162: | ||
} | } | ||
− | }</ | + | }</syntaxhighlight> |
Zeile 191: | Zeile 190: | ||
Abgesehen von den Einrichtungsschwierigkeiten für komplexe Probleme '''erreicht diese Lösung ihre Grenzen''', sobald ein kritischer Abschnitt mit mehreren Locks synchronisiert werden muss. | Abgesehen von den Einrichtungsschwierigkeiten für komplexe Probleme '''erreicht diese Lösung ihre Grenzen''', sobald ein kritischer Abschnitt mit mehreren Locks synchronisiert werden muss. | ||
− | < | + | <syntaxhighlight lang="java">class EineKlasse { |
private final Object lock1 = new Object () ; | private final Object lock1 = new Object () ; | ||
Zeile 210: | Zeile 209: | ||
} | } | ||
− | }</ | + | }</syntaxhighlight> |
− | In der Tat, um das wait() anzuwenden sind wir gezwungen, einen der zwei Locks zu wählen, und demnach wird lediglich ein einziger der beiden Locks während des Wartens freigegeben. Im vorliegenden Fall verriegelt der Aufruf | + | In der Tat, um das wait() anzuwenden sind wir gezwungen, einen der zwei Locks zu wählen, und demnach wird lediglich ein einziger der beiden Locks während des Wartens freigegeben. Im vorliegenden Fall verriegelt der Aufruf methode() gänzlich den lock1, und zwar so lange wie lock2.wait() nicht zu Ende gekommen ist. |
== Das Paket java.util.concurrent.locks == | == Das Paket java.util.concurrent.locks == | ||
− | Die Lösung, die wir oben bereits gesehen haben, um den gegenseitigen Ausschluss unter Benutzung dieses Pakets zu verwirklichen, erlaubt es uns auch, über die Grenzen der Methoden der Object-Klasse hinauszugehen. Dabei besteht die Grundidee darin, nur ''' | + | Die Lösung, die wir oben bereits gesehen haben, um den gegenseitigen Ausschluss unter Benutzung dieses Pakets zu verwirklichen, erlaubt es uns auch, über die Grenzen der Methoden der Object-Klasse hinauszugehen. Dabei besteht die Grundidee darin, nur '''einen''' Lock zu benutzen, aber mit mehreren Bedingungsvariablen, auf denen wir die Operationen durchführen können, die ähnlich sind wie wait() und notify(). |
− | < | + | <syntaxhighlight lang="java">class EineKlasse { |
private final Lock lock = new ReentrantLock () ; | private final Lock lock = new ReentrantLock () ; | ||
Zeile 246: | Zeile 245: | ||
} | } | ||
− | }</ | + | }</syntaxhighlight> |
In methode1() zum Beispiel gibt der Aufruf von cond2.await() den lock frei. | In methode1() zum Beispiel gibt der Aufruf von cond2.await() den lock frei. | ||
Zeile 294: | Zeile 293: | ||
Hier ein sehr besonderes Beispiel eines Semaphors mit nur einer Münzmarke: | Hier ein sehr besonderes Beispiel eines Semaphors mit nur einer Münzmarke: | ||
− | < | + | <syntaxhighlight lang="java">Semaphore sem = new Semaphore ( 1 ) ; |
try { | try { | ||
sem.acquire () ; | sem.acquire () ; | ||
Zeile 301: | Zeile 300: | ||
} catch ( InterruptedException e ) { | } catch ( InterruptedException e ) { | ||
e.printStackTrace () ; | e.printStackTrace () ; | ||
− | }</ | + | }</syntaxhighlight> |
Ein Semaphor mit nur einer Münzmarke ist einem Lock sehr ähnlich. | Ein Semaphor mit nur einer Münzmarke ist einem Lock sehr ähnlich. | ||
Zeile 317: | Zeile 316: | ||
Hier ist ein Beispiel der Benutzung von Executor: | Hier ist ein Beispiel der Benutzung von Executor: | ||
− | < | + | <syntaxhighlight lang="java">Executor executor = Executors.newSingleThreadExecutor () ; |
executor.execute ( new Runnable () { | executor.execute ( new Runnable () { | ||
− | + | public void run () { | |
System.out.println ( "asynchroner Aufruf 1" ) ; | System.out.println ( "asynchroner Aufruf 1" ) ; | ||
} | } | ||
}) ; | }) ; | ||
executor.execute ( new Runnable () { | executor.execute ( new Runnable () { | ||
− | + | public void run () { | |
System.out.println ( "asynchroner Aufruf 2" ) ; | System.out.println ( "asynchroner Aufruf 2" ) ; | ||
} | } | ||
− | }) ;</ | + | }) ;</syntaxhighlight> |
Zeile 333: | Zeile 332: | ||
Andere Werkzeuge sind ebenfalls verfügbar, konsultiere dazu bitte die Dokumentation. | Andere Werkzeuge sind ebenfalls verfügbar, konsultiere dazu bitte die Dokumentation. | ||
− | |||
= Gegenseitiges Blockieren = | = Gegenseitiges Blockieren = | ||
Zeile 341: | Zeile 339: | ||
Hier ein sehr einfaches Beispiel des gegenseitigen Blockierens: | Hier ein sehr einfaches Beispiel des gegenseitigen Blockierens: | ||
− | < | + | <syntaxhighlight lang="java">class DeadLock { |
private final Object lock1 = new Object () ; | private final Object lock1 = new Object () ; | ||
Zeile 356: | Zeile 354: | ||
} | } | ||
− | }</ | + | }</syntaxhighlight> |
Aktuelle Version vom 9. März 2018, 15:05 Uhr
Autor: rom1v von www.javafr.com
Aus dem Französischen übersetzt von André Uhres, mit freundlicher Genehmigung des Autors
Inhaltsverzeichnis
Einführung
Die Synchronisation ist ein wesentliches Element, sobald mehrere Threads benutzt werden (das heißt in fast allen Anwendungen). In der Tat, ohne Synchronisation ist es unmöglich, eine robuste Anwendung zu entwickeln, die funktioniert, ungeachtet der Verflechtungen ihrer Threads.
Beschreibung der allgemeinen Probleme
Sehen wir uns zunächst ein klassisches Synchronisationsproblem an. Mehrere Empfänger-Einheiten (Threads) erhalten Kundenaufträge. Sie können die Aufträge in einer Warteschlange ablegen. Die Arbeiter nehmen die abgelegten Arbeiten entgegen, erledigen sie, und liefern ein Ergebnis in einer zweiten Warteschlange. Der Sender holt sich diese Ergebnisse und kann sie dann an den Kunden senden.
Mehrere Synchronisationsprobleme stellen sich hier:
- Mehrere Empfänger können nicht gleichzeitig eine Arbeit in die Warteschlange der Arbeiten ablegen, andernfalls wären diese Aufträge in demselben Kasten;
- Wenn die Warteschlange der Arbeiten voll ist, müssen die Empfänger darauf warten, dass ein Kasten frei wird, um eine neue Arbeit abzulegen;
- Mehrere Arbeiter können nicht jeder gleichzeitig eine Arbeit nehmen, andernfalls hätten sie dieselbe Arbeit zu erledigen;
- Wenn die Warteschlange der Arbeiten leer ist, müssen die Arbeiter darauf warten, dass eine Arbeit abgelegt wird, um sie ausführen zu können;
- Mehrere Arbeiter können nicht gleichzeitig ein Arbeitsergebnis in die Warteschlange der Ergebnisse ablegen, andernfalls wären diese Ergebnisse in demselben Kasten;
- Wenn die Warteschlange der Ergebnisse voll ist, müssen die Arbeiter darauf warten, dass ein Kasten frei wird, um ein neues Ergebnis abzulegen;
- Wenn die Warteschlange der Ergebnisse leer ist, muss der Sender darauf warten, dass ein Ergebnis verfügbar wird.
Zielsetzungen
Das Ziel besteht darin, diese allgemeinen Probleme zu lösen, welche die meisten Fälle abdecken. Um sie richtig zu lösen, muss man folgendes sicherstellen:
- Die Sicherheit: nichts Verkehrtes ereignet sich, ungeachtet der Verflechtung der Threads (zwei Arbeiter können nie dieselbe Arbeit nehmen);
- Die Schnelligkeit: es wird schließlich etwas Richtiges geschehen (wenn die Warteschlange der Arbeiten nicht leer ist, wird ein Arbeiter schließlich die Arbeiten erledigen).
Man muss auch die möglichen Fälle berücksichtigen; zum Beispiel können zwei nacheinander abgelegte Arbeiten folgerichtig vom selben Arbeiter ausgeführt werden.
Gegenseitiger Ausschluss von kritischen Abschnitten
Der gegenseitige Ausschluss erlaubt, die Synchronisationsprobleme 1, 3 und 5 aus der Beschreibung der allgemeinen Probleme zu lösen. Hier ein Programmbeispiel, das die Sicherheit nicht respektiert, wenn mehrere Threads dort gleichzeitig Zugang haben :
class StringList {
private String[] list = new String[ 50 ];
private int index = 0 ;
void add ( String s ) {
list [ index ] = s;
index++;
}
}
Dieses Programm, wie du es geraten haben wirst, erlaubt es, eine Liste von Strings zu verwalten, indem es ein Array benutzt.
void add ( String s ) { void add ( String s ) {
list[index] = s; //(a1) list[index] = s; //(b1)
index++; //(a2) index++; //(b2)
}
Stellen wir uns jetzt zwei Threads vor, T1 und T2, die parallel (oder pseudoparallel auf einem Monoprozessor) auf derselben StringList die Funktion „add(String)“ ausführen. Ihre Aktionen können so verflochten werden, dass mehrere Ausführungen möglich sind. Zum Beispiel:
- (a1) (a2) (b1) (b2) ist eine mögliche, zusammenhängende Ausführung.
- (b1) (b2) (a1) (a2) ist eine mögliche, zusammenhängende Ausführung.
- (a1) (b1) (b2) (a2) ist eine mögliche, aber zusammenhanglose Ausführung: die Tabelle enthält die durch T1 hinzugefügte Zeichenfolge nicht, und ein Kasten der Liste ist leer.
Mehrere andere Ausführungen können zu anderen Ergebnissen führen, von denen einige zusammenhanglos sind. Dieses rückläufige Problem kann durch den gegenseitigen Ausschluss gelöst werden. Die Funktion add(String) wird kritischer Abschnitt genannt. Mehrere abhängige kritische Abschnitte dürfen ihren Code nie gleichzeitig ausführen (durch mehrere verschiedene Threads): man sagt, dass sie in gegenseitigem Ausschluss sind. Im vorhergehenden Beispiel ist add(String) in gegenseitigem Ausschluss mit sich selbst, aber man kann sich sicher gut vorstellen, dass sie auch in gegenseitigem Ausschluss mit der Funktion „remove“ ist...
Um den gegenseitigen Ausschluss zu schaffen, muss man Locks (Riegel) benutzen. Wenn ein Thread auf einen kritischen Abschnitt zukommt, verlangt er den Lock. Wenn er ihn erhält, kann er den Code ausführen. Wenn er ihn nicht erhält, weil ein anderer Thread ihn bereits genommen hat, dann wird er blockiert, indem er darauf wartet, ihn zu erhalten. Es ist möglich, eine potentiell unendliche Anzahl von Locks zu benutzen, und demnach präzise gegenseitige Ausschlüsse zu verwirklichen: zum Beispiel muss a() in gegenseitigem Ausschluss mit sich selbst und mit b() sein, während c() in gegenseitigem Ausschluss mit sich selbst und mit d()sein muss, aber nicht mit a() und b()...
Ein kritischer Abschnitt wird von einem anderen kritischen Abschnitt, der denselben Lock benutzt, als eine atomare Operation angesehen (eine einzige unteilbare Operation).
Das Schlüsselwort synchronized
Um dies in Java zu verwirklichen, besteht die einfachste Methode darin, das Schlüsselwort synchronized zu benutzen. Hier sieht man, wie es benutzt wird:
synchronized ( einObjekt ) {
//kritischer Abschnitt
}
„einObjekt“ stellt einen Lock (Riegel) dar, welcher irgendein Java Objekt sein kann. Aber Vorsicht, es ist besser, Referenzen zu benutzen, die als „final“ deklariert wurden, um sicher zu sein, dass die Referenz auf das Objekt nicht geändert wird; in der Tat, der nachfolgende Code funktioniert nicht:
synchronized (meineListe) {
meineListe = new ArrayList<String>() ;
}
Wenn mehrere Threads im synchronisierten Block ankommen, referenziert die Variable "meineListe" nicht immer dasselbe Objekt und stellt also nicht immer denselben Lock dar.
Ein besonderer Fall: Wenn das Objekt, das als Lock für die Synchronisation dient, this ist und den ganzen Code einer Methode einschließt, kann man das Schlüsselwort synchronized auch in den Methodenkopf setzen. Die zwei nachstehenden Codes sind somit absolut gleichwertig:
void methode () {
synchronized ( this ) {
//kritischer Abschnitt
}
}
synchronized void methode () {
//kritischer Abschnitt
}
Das Paket java.util.concurrent.locks
Eine andere Methode, um Locks zu verwirklichen, ist erstmals in Java 1.5. erschienen. Sie kann auf den ersten Blick schwieriger scheinen, aber sie ist machtvoller, da sie erlaubt, Sachen zu machen, die das Schlüsselwort synchronized nicht erlaubt (wir werden später darauf zurückkommen). Hier siehst du, wie sie zu benutzen ist:
Lock l;
...
l = new ReentrantLock();
...
l.lock();
try {
//kritischer Abschnitt
} finally {
l.unlock();
}
Um diese Methode mit der vorhergehenden zu vergleichen:
l.lock () und l.unlock () entsprechen dem Beginn bzw. Schluss des synchronized Blocks.
Jedoch, wenn du einen dermaßen einfachen gegenseitigen Ausschluss machen willst, wie jene der hier vorgestellten Beispiele, rate ich dir synchronized zu benutzen und die Methode des Pakets java.util.concurrent.locks für recht besondere Fälle zu reservieren, die wir später sehen werden.
Gemeinsame Synchronisation
Die gemeinsame Synchronisation erlaubt es uns, die Synchronisationsprobleme 2, 4, 6 und 7 aus obiger Beschreibung der allgemeinen Probleme zu lösen.
Methoden der Klasse Object
Benutzung
Die grundlegendste Art der Synchronisation zwischen mehreren Java Threads ist die Benutzung der Methoden wait(), notify() und eventuell notifyAll(), die in der Object-Klasse definiert sind. Um ihre Funktionsweise zu begreifen, vervollständigen wir unser Beispiel von soeben:
class StringList {
private String [] list = new String [ 50 ] ;
private int index = 0 ;
synchronized void add(String s) {
list [ index ] = s;
index++;
notify();//<-------------------------------------------------------
System.out.println("notify() ausgeführt");
}
synchronized String getFirstBlockingElement () {
//while the list is empty
while ( index == 0 ) {
try {
//wait passively
wait();//<-------------------------------------------------------
} catch ( InterruptedException ie ) {
ie.printStackTrace () ;
}
}
return list [ 0 ] ;
}
}
Die Methode getFirstBlockingElement gibt das erste Element der Liste zurück; wenn die Liste leer ist, wartet sie darauf, dass ein Element hinzugefügt wird.
Du bist vielleicht überrascht wegen der while Schleife der Methode getFirstBlockingElement, anstelle eines einfachen if. Wir werden im einzelnen sehen, was sich dort abspielt.
Nehmen wir an, dass der Thread T1 add(..) ausführt, und dass der Thread T2 getFirstBlockingElement ausführt.
Nehmen wir weiter an, dass T2 zuerst die Hand hat, er nimmt den Lock (die Methode wurde als synchronized deklariert), er findet index == 0 wahr, also führt er wait() aus. Dieses wait() ist blockierend so lange wie es nicht durch ein notify() auf demselben Objekt freigegeben wird.
Bemerkung: Wenn mehrere Threads einObjekt.wait() ausführen, dann wird jedes einObjekt.notify() einen blockierten Thread freigeben, in einer unbestimmten Reihenfolge.
Jetzt nimmt T1 die Hand. Er führt add(..) aus, und verlangt also den Lock. Aber du wirst mir sagen, er wird ihn nicht erhalten, denn es ist T2, der den Lock hat! Und dennoch wird er ihn erhalten, denn T2 hat den Lock in dem Moment losgelassen, wo er die wait() Methode aufgerufen hat!
Bemerkung: Der Aufruf der Methode wait() gibt den Lock nur frei, weil das Objekt, auf dem wait() angewendet worden ist, dasselbe ist wie der Lock (hier this).
T1 kann also den Code der Methode add(..) ausführen. Wenn er notify() ausführt, gibt er T2 frei (asynchron). Aber jetzt hat T2 natürlich noch nicht den Lock, da T1 ihn noch braucht um System.out.println(...) auszuführen. T2 ist also blockiert mit dem Warten auf den Lock, und andere Threads (stellen wir uns vor T3, T4...), können ihn vor ihm erhalten. Nehmen wir an, dass T3 alle Elemente der Liste entfernt (durch eine mögliche clear() Methode), und dass danach T2 die Hand nimmt. T2 kommt dann aus dem wait() heraus. Er muss dann also noch einmal die Bedingung index == 0 prüfen, andernfalls würde er versuchen, das Element am Index 0 des Arrays wiederzugewinnen, das leer ist. Das ist der Grund, weswegen man die while Schleife benutzen muss!
Bemerkung: Man darf vor allem diese while Schleife nicht mit einem aktiven Warten verwechseln, das ständig prüft, ob der Index 0 ist. Hier schauen wir ob der Index 0 ist, wenn es der Fall ist, setzen wir den Thread in einen passiven Wartezustand, der nur geweckt wird, wenn ein Element hinzugefügt wird. Wenn er einmal geweckt ist, muss er aber die Bedingung neu prüfen (aus dem Grund, den wir soeben gesehen haben).
Diese Art vorzugehen ist für einfache Synchronisierungsprobleme praktisch; für komplexere Probleme ist es besser, andere Methoden zu benutzen. Im vorliegenden Fall ist die blockierende Liste, die Gegenstand unseres Beispiels war, gänzlich in der Java 1.5 API codiert, wir werden noch darauf zurückkommen.
Grenzen
Abgesehen von den Einrichtungsschwierigkeiten für komplexe Probleme erreicht diese Lösung ihre Grenzen, sobald ein kritischer Abschnitt mit mehreren Locks synchronisiert werden muss.
class EineKlasse {
private final Object lock1 = new Object () ;
private final Object lock2 = new Object () ;
void methode () {
synchronized ( lock1 ) {
synchronized ( lock2 ) {
while ( bedingung ) {
lock2.wait () ;
}
}
}
}
void benachrichtigen() {
lock2.notify () ;
}
}
In der Tat, um das wait() anzuwenden sind wir gezwungen, einen der zwei Locks zu wählen, und demnach wird lediglich ein einziger der beiden Locks während des Wartens freigegeben. Im vorliegenden Fall verriegelt der Aufruf methode() gänzlich den lock1, und zwar so lange wie lock2.wait() nicht zu Ende gekommen ist.
Das Paket java.util.concurrent.locks
Die Lösung, die wir oben bereits gesehen haben, um den gegenseitigen Ausschluss unter Benutzung dieses Pakets zu verwirklichen, erlaubt es uns auch, über die Grenzen der Methoden der Object-Klasse hinauszugehen. Dabei besteht die Grundidee darin, nur einen Lock zu benutzen, aber mit mehreren Bedingungsvariablen, auf denen wir die Operationen durchführen können, die ähnlich sind wie wait() und notify().
class EineKlasse {
private final Lock lock = new ReentrantLock () ;
private final Condition cond1 = lock.newCondition () ;
private final Condition cond2 = lock.newCondition () ;
void methode1 () throws InterruptedException {
lock.lock () ;
try {
cond2.await () ;
cond1.signal () ;
} finally {
lock.unlock () ;
}
}
void methode2 () throws InterruptedException {
lock.lock () ;
try {
cond1.await () ;
cond2.signal () ;
} finally {
lock.unlock () ;
}
}
}
In methode1() zum Beispiel gibt der Aufruf von cond2.await() den lock frei. Eine ReadWriteLock-Schnittstelle ist in diesem Paket auch verfügbar, die erlaubt, die Lesen-Schreiben Probleme zu lösen (jederzeit entweder irgendeine Anzahl von Lesern, oder einen einzigen Schreiber). Ich lasse dich die Dokumentation für seine Benutzung konsultieren. Aber es genügt vorläufig zu wissen, dass das besteht.
Threads
Bis jetzt bezeichnete das Wort „Thread“ einen parallelen Vorgang. In diesem Abschnitt wird das Wort „Thread“ die Thread-Klasse von Java bedeuten. Die Beziehung, die zwischen den zwei besteht, ist, dass der Aufruf der Methode start () der Thread-Klasse einen neuen Thread schafft, in dem der Code ausgeführt wird.
Dies ist kein Artikel über die Thread-Klasse, sondern hier werden lediglich einige Methoden angeführt, die man kennen sollte:
static Thread currentThread (): Erlaubt, den laufenden Thread zu erhalten.
static void yield (): Gibt den anderen Threads eine Chance, sich auszuführen.
static void sleep(long ms) throws InterruptedException: legt den aufrufenden Thread während der angegebenen Zeit schlafen (dies bitte nicht zum Zweck der Synchronisation benutzen!).
void interrupt (): verursacht entweder das Werfen der InterruptedException wenn die Aktivität auf einer Synchronisationsoperation blockiert, oder das Setzen eines interrupted Indikators.
void join (): Blockierendes Warten auf den Abschluss eines Threads, also bis die mit dem Thread verbundene run() Methode beendet ist.
Semaphoren
Es ist wohl unmöglich ein Tutorial über die Synchronisation zu schreiben, ohne über die Semaphoren zu sprechen. In einer theoretischeren Beschreibung der Synchronisationsprobleme würden die Semaphoren vor den zwei vorhergehenden Lösungen beschrieben werden; du wirst jedoch die Semaphoren in Java wohl kaum benutzen müssen. Aber ich glaube, dass das Verständnis ihrer Funktionsweise wichtig ist.
Die Semaphore-Klasse ist in Java 1.5. erschienen.
Definition
Ein Semaphor verkapselt einen strikt positiven Integer und zwei atomare (also unteilbare) Hoch- und Runterzähloperationen.
- Integer (stets positiv oder null).
- Operation acquire(): zählt den Zähler runter, wenn er strikt positiv ist; blockiert, wenn er gleich null ist, indem sie darauf wartet, runterzählen zu können.
- Operation release(): zählt den Zähler hoch.
Man kann ein Semaphor als eine Sammlung von Münzmarken mit zwei Operationen sehen:
- Eine Münzmarke nehmen, indem man wenn nötig darauf wartet, dass welche vorhanden sind;
- Eine Münzmarke ablegen.
Bemerkung: Die abgelegten Münzmarken sind nicht notgedrungen jene, die genommen worden sind.
Benutzung
Hier ein sehr besonderes Beispiel eines Semaphors mit nur einer Münzmarke:
Semaphore sem = new Semaphore ( 1 ) ;
try {
sem.acquire () ;
//kritischer Abschnitt
sem.release () ;
} catch ( InterruptedException e ) {
e.printStackTrace () ;
}
Ein Semaphor mit nur einer Münzmarke ist einem Lock sehr ähnlich. Jedoch darf man die beiden Begriffe nicht miteinander verwechseln! In der Tat, wenn man mehrere Mal die Operation release() durchführt, behält ein Semaphor diese Anträge im Gedächtnis indem er den Integer hochzählt, den er benutzt; für einen Lock ist die Durchführung von mehreren Entriegelungen absolut gleichbedeutend mit der Durchführung von nur einer einzigen Entriegelung. So könnte man mehrere aufeinanderfolgende release() Operationen auf einem Semaphor als „verzögerte“ notify() Operationen auf einem Lock ansehen.
In der API 1.5 eingerichtete Lösungen
Version 1.5 von Java und sein Paket java.util.concurrent (sowie seine Unterpakete) liefern Synchronisationswerkzeuge von höherem Niveau.
Zum Beispiel wird das Beispiel, das wir benutzt haben, um eine blockierende Liste zu simulieren, durch BlockingQueue (nicht begrenzte blockierende Liste) und ArrayBlockingQueue (blockierende Liste mit begrenztem Puffer) verwirklicht.
Ein anderes sehr nützliches Werkzeug ist Executor. Es handelt sich um eine blockierende Warteliste von durchzuführenden Aktionen. Man findet genau denselben Mechanismus wieder, wenn man den Thread benutzt, welcher der graphischen Anzeige von Swing gewidmet ist (Event Dispatch Thread): SwingUtilities.invokeLater(Runnable) stellt eine Aktion in die Warteschlange, die in dem Thread durchzuführen ist, welcher der graphischen Anzeige gewidmet ist.
Hier ist ein Beispiel der Benutzung von Executor:
Executor executor = Executors.newSingleThreadExecutor () ;
executor.execute ( new Runnable () {
public void run () {
System.out.println ( "asynchroner Aufruf 1" ) ;
}
}) ;
executor.execute ( new Runnable () {
public void run () {
System.out.println ( "asynchroner Aufruf 2" ) ;
}
}) ;
Dieses Beispiel führt Runnables asynchron gegenüber dem laufenden Thread aus, aber gewährleistet, dass sie in der Aufrufreihenfolge nacheinander durchgeführt werden.
Andere Werkzeuge sind ebenfalls verfügbar, konsultiere dazu bitte die Dokumentation.
Gegenseitiges Blockieren
Das bekannteste Synchronisationsproblem bei der Entwicklung ist das gegenseitige Blockieren. Ein gegenseitiges Blockieren (oder Deadlock), ist ein Blockieren, das erfolgt, wenn zum Beispiel Thread A auf Thread B wartet, während gleichzeitig B auf A wartet.
Hier ein sehr einfaches Beispiel des gegenseitigen Blockierens:
class DeadLock {
private final Object lock1 = new Object () ;
private final Object lock2 = new Object () ;
void a () throws InterruptedException {
lock1.wait () ;
lock2.notify () ;
}
void b () throws InterruptedException {
lock2.wait () ;
lock1.notify () ;
}
}
Wenn Thread T1 a() ausführt, und Thread T2 b() ausführt, gibt es gegenseitiges Blockieren. In der Tat erwartet T1 das notify() von T2, aber, damit T2 das notify() aufrufen kann, ist es zuerst notwendig, dass T1 sein notify() ausführt...
Man muss unbedingt darauf achten, nie ein gegenseitiges Blockieren zu verursachen.
Unnötige Synchronisation vermeiden
Die Synchronisation ist in vielen Situationen etwas Wesentliches. Jedoch ist sie teuer an Prozessormitteln: man sollte daher nicht unbedingt alles synchronisieren. Wenn eine Methode nur von einem einzigen Thread aufgerufen werden kann, synchronisiere sie nicht. Die Methoden von Swing Listeners werden notgedrungen im EDT aufgerufen, es ist also unnötig, irgendwas zu synchronisieren (außer, wenn du von dort aus neue Threads anlegst).
Wenn du viel Synchronisation hast, versuch zu schauen, ob das Modell einer einzigen Warteschlange sich nicht besser eignet (Executor), das wird viel von deinem Code vereinfachen...
Vermeide die alten Collections, die von Java 1.0 kommen (Vector, Hashtable...) und von Haus aus synchronisiert sind. Benutze dagegen die neuen, die ab Java 1.2 gekommen sind (ArrayList, HashMap...) und nicht synchronisiert werden (bessere Leistungen). Um eine synchronisierte Sicht einer Collection wiederzugewinnen reicht es aus, Collections.synchronizedCollection (Collection) aufzurufen.