15.33 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 namens 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 lässt sich nicht fortsetzen, weil der AWT-Thread blockiert ist. Bei einer Aktion in einem Event-Handler sollten wir einen separaten 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();
}
};
|
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 dies jedoch etwas anders, wie wir im nächsten Abschnitt sehen werden.
15.33.1 Swing ist nicht Thread-sicher
 
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. |
|
Der Verzicht auf Synchronisation bringt einen Gewinn an Ausführungsgeschwindigkeit |
Beispiel Swing weiß mit konkurrierenden Zugriffen nicht allzu viel anzufangen.
|
Listing 15.48
SwingNoSyncDemo.java
import javax.swing.*;
public class SwingNoSyncDemo
{
public static void main( String[] args )
{
final DefaultListModel model = new DefaultListModel();
JFrame frame = new JFrame();
frame.add( new JList( model ) );
frame.setSize( 200, 100 );
frame.setVisible( true );
new Thread() {
@Override public void run() {
setPriority( Thread.MIN_PRIORITY );
while ( true )
model.addElement( "Dumm gelaufen" );
}
}.start();
new Thread() {
@Override 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 in thread "AWT-EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: 14 >= 14
at java.util.Vector.elementAt(Vector.java:432)
at javax.swing.DefaultListModel.getElementAt(DefaultListModel.java:70)
...
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:149)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:110)
Exception in thread "AWT-EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: 0
at javax.swing.plaf.basic.BasicListUI.updateLayoutState(BasicListUI.java:1155)
at javax.swing.plaf.basic.BasicListUI.maybeUpdateLayoutState(BasicListUI.
java:1098)
at javax.swing.plaf.basic.BasicListUI.paint(BasicListUI.java:234)
at javax.swing.plaf.ComponentUI.update(ComponentUI.java:142)
...
Obwohl als unterliegende Datenstruktur der Vektor vorhanden ist, der, wie wir wissen, nur synchronisierte Methoden besitzt, ist nicht er direkt der Übeltäter. Es liegt an Swing, wie mit den Daten umgegangen wird. Wenn der erste Thread Daten in das Model einfügt, muss die Visualisierung aktualisiert werden. Als Datenstruktur nimmt das Standardmodel einen java.util.Vector, der die Daten aufnimmt. Das Model informiert also das Darstellungsobjekt darüber, dass es den Inhalt neu zeichnen muss. Merken wir uns die Stelle. Das Darstellungsobjekt wird sich nun vom Model die Daten besorgen. Bis dahin läuft alles ganz gut. Doch der zweite Thread löscht parallel die Daten aus dem Model. 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 jedoch mindestens ein Wert. Daher folgt eine ArrayIndexOutOfBoundsException in der Methode elementAt() vom Vektor. Die Visualisierung fragt mit einem Index im Vektor nach, doch der Vektor hat vom Lösch-Thread schon ein Element abgeben müssen. Daher ist die interne Größe des Vektors kleiner als der von Swing erfragte Index.
Lösung für Swing
Einige der Methoden, die dennoch synchronisiert sind, tragen Listener ein, so etwa bei JComponent addPropertyChangeListener(), removePropertyChangeListener() und add-VetoableChangeListener(), 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.33.2 Swing-Elemente bedienen mit invokeLater(), 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 gibt es 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 invoke-AndWait() durchlaufen werden. Den Methoden wird ein Runnable-Objekt übergeben. Die beiden 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 ist in der Regel wenig sinnvoll, die Methode so lange stehen zu lassen, bis die Anzeige auch wirklich gezeichnet wurde.
Ein Problem stellt für sehr viele Applikationen leider die Tatsache dar, 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; wir wollen von der Implementierung nur wenige Zeilen betrachten. Im Prinzip leistet die Methode das Gleiche wie invokeLater(), auch sie muss das InvocationEvent in die Warteschlange legen. Hinzu kommt jedoch, 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 als Argument wieder 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 invoke-AndWait() aus dem synchronized-Block aus, und es geht weiter.
|