15.34 AWT, Swing und die Threads
Beim AWT und bei Swing gibt es einen Thread, der für die Oberflächenelemente verantwortlich ist: den AWT-Thread. Er läuft parallel zum Hauptprogramm (ein Thread mit dem Namen main) und führt den Programmcode in den Listenern aus. Aus diesem Grund ist es auch ungünstig, in einen Event-Handler langen Programmcode zu legen, denn dann steht die grafische Applikation und kann nicht weitermachen, da der AWT-Thread blockiert ist. Wenn wir eine Aktion in einem Event-Handler machen müssen, dann sollten wir einen extra Thread starten, damit die grafische Oberfläche sofort wieder reaktionsfähig ist.
Beispiel Wenn eine Schaltfläche gedrückt wird, soll ein langer Text in den Puffer eingelesen werden. Das lässt sich schön mit zwei inneren Klassen realisieren.
|
ActionListener al = new ActionListener() {
public void actionPerformed( ActionEvent e ) {
New Thread( new ReaderThread(e.getActionCommand()) ).start();
System.exit( 0 );
}
};
|
In einer externen Klasse lesen wir den Text:
class ReaderThread implements Runnable
{
ReaderThread( String actionCommand )
{
// ...
}
public void run() {
// ...
}
}
Unter dem AWT ist es kein Problem, wenn zwei Threads auf ein und dasselbe Oberflächenelement zugreifen. Bei Swing ist das jedoch etwas anders, wie wir im nächsten Abschnitt sehen werden.
15.34.1 Warum Swing nicht Thread-sicher ist
Die Tatsache, dass das Swing-Toolkit nicht Thread-sicher ist, erstaunt vielleicht auf den ersten Blick. Das AWT ist Thread-sicher, da AWT auf Plattform-Peer-Elemente vertraut. In einer List-Box unter dem AWT ist es problemlos möglich, ein Element einzufügen und parallel zu löschen. Doch auf die Synchronisation bei Swing wurde aus zwei Gründen verzichtet:
|
Untersuchungen mit anderen grafischen Bibliotheken haben ergeben, dass Operationen in Threads zu ärgerlichen Deadlock-Situationen führen können. Es ist eine zusätzliche Last, die auf dem Programmieren grafischer Oberflächen lastet, Monitore korrekt einzusetzen. |
|
Als zweiter, sicherlich in Zukunft weniger wichtiger Punkt ist der Gewinn von Ausführungsgeschwindigkeit zu nennen. Das Swing-Toolkit kostet ohnehin viel Zeit, so dass auf die zusätzliche Synchronisation gut verzichtet werden kann. |
Beispiel Swing kann mit konkurrierenden Zugriffen nicht viel anfangen.
|
Listing 15.46 SwingNoSyncDemo.java
import javax.swing.*;
public class SwingNoSyncDemo
{
public static void main( String args[] )
{
final DefaultListModel model = new DefaultListModel();
JList list = new JList( model );
JFrame frame = new JFrame();
frame.getContentPane().add( list );
frame.setSize( 200, 100 );
frame.show();
new Thread(){ public void run() {
setPriority( Thread.MIN_PRIORITY );
while ( true )
model.addElement( "Dumm gelaufen" );
}
}.start();
new Thread(){ public void run() {
setPriority( Thread.MIN_PRIORITY );
while ( true )
model.removeElement( "Dumm gelaufen" );
}
}.start();
}
}
Werfen wir einen Blick auf die Ausgabe, die erscheint, wenn das Programm nur kurz läuft:
Exception occurred during event dispatching:
java.lang.ArrayIndexOutOfBoundsException: 4145 >= 4145
at java.util.Vector.elementAt(Vector.java:417)
at javax.swing.DefaultListModel.getElementAt(DefaultListModel.java:70)
...
at java.awt.Component.dispatchEvent(Component.java:2499)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:319)
at java.awt.EventDispatchThread.pumpOneEvent(EventDispatchThread.java:103)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:84)
Exception occurred during event dispatching:
java.lang.ArrayIndexOutOfBoundsException: 4288 >= 4288
at java.util.Vector.elementAt(Vector.java:417)
at javax.swing.DefaultListModel.getElementAt(DefaultListModel.java:70)
...
Obwohl als unterliegende Datenstruktur der Vektor vorhanden ist, der, wie wir wissen, nur synchronisierte Methoden besitzt, ist er nicht direkt der Übeltäter. Es liegt an Swing, wie mit den Daten umgegangen wird. Wenn der erste Thread Daten in das Modell einfügt, muss die Visualisierung aktualisiert werden. Als Datenstruktur nimmt das Standardmodell einen Vektor, der die Daten aufnimmt. Das Modell informiert also das Darstellungsobjekt, dass es den Inhalt neu zeichnen muss. Merken wir uns die Stelle. Das Darstellungsobjekt wird sich nun vom Modell die Daten besorgen. Bis dahin läuft alles ganz gut. Doch der zweite Thread löscht parallel die Daten aus dem Modell. Springen wir jetzt zur Markierung zurück. Irgendwann passiert es, dass zwischen der Benachrichtigung der Darstellungskomponenten und dem wirklichen Zeichnen etwas gelöscht wird. Die Visualisierung weiß aber davon nichts und versucht alle Werte zu zeichnen; es fehlt aber mindestens ein Wert. Daher folgt eine ArrayIndexOutOfBoundsException in der Methode elementAt() vom Vektor:
java.lang.ArrayIndexOutOfBoundsException: 4145 >= 4145
at java.util.Vector.elementAt(Vector.java:417)
Werfen wir einen Blick in die Implementierung, dann erkennen wir das Problem:
public synchronized Object elementAt( int index ) {
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(
index + " >= " + elementCount);
}
...
}
Die Visualisierung fragt mit dem Index 4145 im Vektor nach, doch der Vektor hat vom Lösch-Thread schon ein Element abgeben müssen. Daher ist die interne Größe elementCount auch kleiner als der Index.
Dass das Programm dennoch eine Zeit lang läuft, hängt damit zusammen, dass der Hinzufüge-Thread aufgeholt haben kann und noch viele Elemente im Vektor auf Vorrat liegen. Nehmen wir setPriority(Thread.MIN_PRIORITY) aus dem Einfüge-Thread heraus, dann gibt es kein Problem.
Lösung für Swing
Einige der Methoden, die dennoch synchronisiert sind, tragen Listener ein, so etwa bei JComponent addPropertyChangeListener(), removePropertyChangeListener() und addVetoableChangeListener(), removeVetoableChangeListener(). Bei JCheckBoxMenuItem ist es dann die einsame Methode setState(boolean), die synchronisiert ist. Es findet sich intern mal hier mal da ein synchronisierter Block. Ansonsten ist jedoch nicht viel dabei, und wir müssen unsere Teile synchronisiert ausführen.
Um Programmstücke konform ausführen zu lassen, definiert Swing einige Methoden und Klassen. Dazu gehören:
|
invokeLater(Runnable) |
|
invokeAndWait(Runnable) |
|
JProgressBar |
|
ProgressMonitor |
|
ProgressMonitorInputStream |
|
SwingWorker |
15.34.2 Swing-Elemente bedienen mit invokeLater() und invokeAndWait()
Da Swing nicht Thread-sicher ist, ist die einzige Möglichkeit zur Manipulation von Oberflächenelementen der AWT-Thread. Wenn wir es schaffen, dort die Aufträge einzureihen, dann wird nichts schief gehen. Genau für diese Aufgabe existieren in der Klasse EventQueue zwei Methoden: invokeLater() und invokeAndWait(). Damit lassen sich beliebige Programmstücke in die Warteschlange einführen. In der Warteschlange für das AWT liegen Aufträge und Ereignisse, die an die Oberflächenelemente verteilt werden. Alles spielt sich dabei neben dem Haupt-Thread ab, so dass Parallelität herrscht. Hat die Warteschlange alle Ereignisbehandler aufgerufen, kann der Programmcode von invokeLater() und invokeAndWait() durchlaufen werden. Den Methoden wird ein Runnable-Objekt übergeben. Die zwei Methoden erfüllen unterschiedliche Bedürfnisse:
|
invokeLater() legt einen Thread in die Warteschlage und kehrt sofort zurück. Die Methode ist somit asynchron. Der Aufrufer weiß nicht, wann der Programmcode abgearbeitet wird. |
|
invokeAndWait() legt ebenfalls den Thread in die Warteschlange, verharrt aber so lange in der Methode, bis der Programmcode in run() aufgerufen wurde. Die Methode ist also synchron. |
Mit diesen Methoden lassen sich jetzt alle Manipulationen an der Oberfläche durchführen.
Beispiel Ein Fortschrittsbalken JProgressBar mit dem Namen bar soll in einer Schleife einer Berechnung angepasst werden.
EventQueue.invokeLater( new Runnable()
{
public void run() {
bar.setValue( i );
}
} );
|
Bei der Auswahl der beiden Funktionen haben wir uns für den Fortschrittsbalken für invokeLater() entschieden. Es macht in der Regel wenig Sinn, die Methode so lange stehen zu lassen, bis die Anzeige auch wirklich gezeichnet wurde.
Ein Problem ist leider für sehr viele Applikationen, dass das Objekt zur Manipulation immer irgendwie sichtbar sein muss. Hier soll bar einfach direkt für die innere Klasse sichtbar sein.
Die Funktionen invokeLater() und invokeAndWait() befinden sich nicht nur in der Klasse EventQueue, sondern sind noch einmal in der Klasse SwingUtilities untergebracht. Daher ist es gleichgültig, ob wir EventQueue.invokeXXX() oder SwingUtilities.invokeXXX() schreiben. SwingUtilities hat vielleicht den Vorteil, dass das Paket java.awt für die EventQueue nicht importiert werden muss, sonst gibt es aber keinen Unterschied.
Implementierung
Genehmigen wir uns abschließend noch einen kurzen Blick auf die Implementierung. Es lässt sich schon erahnen, dass invokeLater() einfacher ist:
public static void invokeLater( Runnable runnable )
{
Toolkit.getEventQueue().postEvent(
new InvocationEvent(Toolkit.getDefaultToolkit(), runnable));
}
Das Ereignis, welches in die Event-Queue kommt, ist vom Typ InvocationEvent und damit ein AWTEvent. Wir übergeben unser Runnable-Objekt, damit der AWT-Thread später die run()-Methode aufrufen kann.
Die Methode invokeAndWait() ist etwas komplizierter, und wir wollen von der Implementierung nur wenige Zeilen betrachten. Im Prinzip macht die Methode das Gleiche wie invokeLater(), sie muss ebenfalls das InvocationEvent in die Warteschlange legen. Doch hinzu kommt, dass invokeAndWait() auf das Ende des Threads warten muss:
InvocationEvent event = new InvocationEvent(
Toolkit.getDefaultToolkit(), runnable, lock, true);
synchronized (lock) {
Toolkit.getEventQueue().postEvent(event);
lock.wait();
}
Das konstruierte InvocationEvent bekommt wieder als Parameter das runnable. Jetzt erhält es aber zusätzlich ein Lock-Objekt. Wenn der AWT-Thread durch die Ereignis-Warteschlange geht und das InvocationEvent sieht, führt er wieder die run()-Methode aus. Anschließend informiert er über notify() das wartende Objekt. Dann steigt invokeAndWait() aus dem sychronized-Block aus, und es geht weiter.
|