14 Threads und Images 14 - 1 Threads und Images Inhalt Threads Synchronisation Images Images Laden Images Erzeugen Übungsaufgaben Threads Threads sind scheinbar parallel arbeitende Teile eines Programs, die auf denselben Datenbestand zugreifen. Bei zwei parallel arbeitenden getrennten Programmen spricht man von Prozessen. Unsere bisherigen Grafikprogramme arbeiteten schon parallel, ohne daß wir das ausgenutzt haben. Der eine Thread war nämlich das Programm main, und der andere war die Ereignisbehandlung. Über den Thread von main hat unser Programm allerdings keine Kontrolle, da es ihn nicht besitzt. So kann man diesen Thread insbesondere nicht unterbrechen oder warten lassen. Eine typische Anwendung für Multi-Threading sind animierte Applets. Wir wollen zuerst ein Beispiel angeben, wo die aktuelle Zeit in einem Applet tickt. Dazu startet man einen Thread, der im Sekundentakt ein repaint des Applets aufruft. Der Code für die Darstellung der Zeit stammt aus der Lösung einer Übungsaufgabe. Der Sekundentakt wird dadurch erzeugt, daß die Methode sleep() von Thread aufgerufen wird. Diese Methode erzeugt eventuell eine Exception (wenn das Unterbrechen nicht möglich war), die abgefangen werden muß. import java.awt.*; import java.applet.*; import java.util.*; // wegen Date class RedrawThread extends Thread // Der Thread, der alle 1000 ms sein Werk verrichten soll { Applet A; public RedrawThread (Applet a) { A=a; start(); } public void run () { while (true) { try // Notwendig! 14 Threads und Images 14 - 2 { sleep(1000); // Warte 1 Sekunde } catch (Exception ex) {} A.repaint(); } } } public class ClockApplet extends Applet { public void init () { new RedrawThread(this); // Erzeuge Thread. Der startet sich selber. } public void paint (Graphics g) { g.drawString(new Date().toString(),50,50); // 50,50 ist ueber den Daumen gepeilt } } Wie man sieht, ist nichts weiter zu tun, als eine Instanz von Thread zu schaffen, und diesen Thread zu starten, indem man seine start()-Methode aufruft. Dies startet einen HintergrundThread mit der Methode run(). Der Thread wird beendet, wenn die Methode run() beendet wird. In unserem Fall hat diese Methode eine Dauerschleife. Das Programm funktioniert so schon. Aber man sollte den Thread unterbrechen, wenn die Seite nicht mehr zu sehen ist, d.h. wenn die stop()-Methode des Applets aufgerufen wurde. In Java 1.0.2. wurde dies einfach dadurch erreicht, daß man die stop()-Methode von Thread aufruft. Aus Sicherheitsgründen ist dies in Java 1.1 nicht die korrekte Vorgehensweise. In diesem Fall geht es ohnehin einfacher, indem man die Dauerschleife von einer Variablen abhängig macht, die man gleich false setzt, wenn der Thread abbrechen soll. import java.awt.*; import java.applet.*; import java.util.*; class RedrawThread extends Thread { Applet A; public boolean Stop; // Stopt den Thread. public RedrawThread (Applet a) { A=a; Stop=false; } public void run () { while (!Stop) // Damit laesst der Thread sich sauber beenden. { ... } } } public class ClockApplet extends Applet { RedrawThread T; public void start () { T=new RedrawThread(this); // Merke Thread } ... public void stop () { T.Stop=true; 14 Threads und Images 14 - 3 } } Nun wird der Thread auch neu gestartet, wenn die Seite angezeigt wird. Beachten Sie, daß dann ein neuer Thread erzeugt wird. Es ist unklar, ob man einen abgelaufenen Thread in jedem Betriebssystem neu starten kann. In jedem Fall kann man aber einen Thread für einen Moment anhalten. Dies geschieht mit der Methode suspend() von Thread. Man kann ihn (natürlich aus einem anderen Thread) wieder mit resume() starten. Den aktuellen Thread ermittelt man mit Thread.currentThread(). Es geht aber noch einfacher. Dazu implementiert unser Applet das Interface Runnable und erzeugt einen Thread, der jedes Objekt, das Runnable implementiert, starten kann. Das Interface hat die einzige Methode run(). Diese Methode wird vom Thread gestartet. import java.awt.*; import java.applet.*; import java.util.*; public class ClockApplet extends Applet implements Runnable { Thread A; boolean Stop; public void paint (Graphics g) { g.drawString(new Date().toString(),50,50); } public void start () { A=new Thread(this); A.start(); Stop=false; } public void stop () { Stop=true; } public void run () { while (!Stop) { try { A.sleep(1000); } catch (Exception ex) {} repaint(); } } } Nun läuft das Applet zufriedenstellend. Hier wurde noch ein Code zum Zentrieren des Strings eingefügt. Synchronisation Wir weisen an dieser Stelle darauf hin, daß Probleme auftreten können, wenn mehrere Threads auf dasselbe Objekt, oder dieselbe Variable zugreifen. Die Threads können sich nämlich nicht darauf verlassen, daß das Objekt für eine bestimmte Zeitspanne unverändert 14 Threads und Images 14 - 4 bleibt. Solchen Problemen muß man mit Synchronisation abhelfen. Dazu kann man einen Block mit einem Objekt synchronisieren. Innerhalb dieses Blocks hat der Thread dann einen sogenannten Lock auf das Objekt. Kein anderer Thread kann in den Block eintreten, bevor das Objekt nicht freigegeben ist. Solch ein Block sieht folgendermaßen aus synchronized (Objekt) Anweisungsblock Alternativ kann man auch eine ganze Methode einer Klasse mit einem Lock auf die Instanz der Klasse, deren Methode aufgerufen wurde, versehen. Dazu erhält die Methode das Schlüsselwort synchronized. Beispiel public synchronized int inc () { int m=n; n++; return m; } Dieses Beispiel erhöht n und gibt den alten Wert zurück. Falls n Variable des Objektes A ist, so kann A.inc() nur in jeweils einem Thread aktiv sein. Dadurch wird verhindert, daß der Wert von n in A inkonsistent ist (siehe Aufgabe 4). Insgesamt ist das Schreiben von Programmen mit mehreren Threads eine Kunst, auf die wir hier nicht weiter eingehen können. Images In diesem Abschnitt werden wir ein Applet schreiben, bei dem ein Ball innerhalb der Fläche des Applets hüpft. Der Ansatz ist zunächst, einen separaten Thread laufen zu lassen, der die Position des Balles neu berechnet und den Ball neu zeichnet. Das folgende Programm implementiert diesen Ansatz. Die Schleife ruft hierbei einfach repaint() auf und legt eine kurze Pause ein. Da wir ohnehin das ganze Applet neu zeichnen, unterbinden wir das Löschen des Applets noch, indem wir update() überschreiben. Normalerweise sorgt diese Funktion für ein Füllen mit der Hintergrundfarbe, die dazu auf die normale Hintergrundfarbe des Applets gesetzt wird. Man kann diese Hintergrundfarbe übrigens setzen und abfragen mit setBackground(color); Color c=getBackground(); Diese Methoden beherrschen alle Komponenten, also auch Applets. Es folgt der erste Versuch für den hüpfenden Ball. import java.awt.*; import java.applet.*; public class BallApplet extends Applet implements Runnable { Thread A; boolean Stop; 14 Threads und Images 14 - 5 int X,Y,DX,DY, // Ballkoordinaten von links unten gesehen W,H; // Hoehe und Weite des Applets final int S=20; // Kugelgroesse als Konstante public void init () { W=size().width; H=size().height; // Bestimme Hoehe und Weite } public void paint (Graphics g) // Hintergrund und Kugel neu Zeichnen { g.setColor(Color.blue.darker()); // gibt ein dunkleres Blau g.fillRect(0,0,W,H); g.setColor(Color.green.darker()); g.fillOval(X-S,H-Y-S,2*S,2*S); } public void update (Graphics g) // Hintergrund wird selbst neu gezeichnet // normalerweise erledigt dies update() { paint(g); } public void start () // Kugel startet in der Mitte unten { X=W/2; Y=S; DX=1; DY=15; A=new Thread(this); A.start(); Stop=false; } public void stop () // Stoppe Thread { Stop=true; } public void run () // Die Schleife { while (!Stop) { if (X+DX<S) DX=-DX; if (X>W-S-1) DX=-DX; if (Y+DY<S) { DY=15; } X+=DX; Y+=DY; DY--; try { A.sleep(10); } catch (Exception e) {} repaint(); } } } Allerdings zeigt die Kugel in dem Beispiel ein deutliches Flimmern. Dies liegt daran, daß zuerst gelöscht wird und dann die Kugel neu gezeichnet wird. Es hilft da auch nichts, wenn man nur den Teil löscht, den die Kugel bedeckt. Wir wollen dennoch eine solche Technik vorstellen. Dazu muß man das Neuzeichnen der Kugel im Thread selbst in Auftrag geben. D.h. man muß sich selbst ein Graphics-Object für das Applet besorgen. Aus Effizienzgründen sollte dieses Objekt anschließend wieder freigegeben 14 Threads und Images 14 - 6 werden. Die Methode run() nimmt damit folgende Gestalt an: public void run () // Die Schleife { while (!Stop) { Graphics g=getGraphics(); g.setColor(Color.blue.darker()); g.fillRect(X-S,H-Y-S,2*S,2*S); if (X+DX<S) DX=-DX; if (X>W-S-1) DX=-DX; if (Y+DY<S) { DY=15; } X+=DX; Y+=DY; DY--; g.setColor(Color.green.darker()); g.fillOval(X-S,H-Y-S,2*S,2*S); g.dispose(); try { A.sleep(10); } catch (Exception e) {} } } Das Flimmern ist immer noch deutlich sichtbar. Man könnte übrigens auf den Gedanken kommen, sich nur einmal ein Graphics-Objekt zu beschaffen und es in der Dauerschleife immer wieder zu verwenden. Dies funktioniert auch in allen Systemen, die mir bekannt sind. Es ist aber unklar, ob dieses Vorgehen manchmal zu Problemen führt, wenn das Fenster verschoben wird oder teilweise verdeckt ist. Die Lösung besteht darin, in ein Hintergrundbild zu zeichnen und dieses Bild erst auf den Schirm zu kopieren, wenn es ganz fertig ist. Man nennt diese Technik Buffering. In Java beschafft man sich einen solchen Puffer mit der createImage()-Methode, die jede Komponente besitzt. Damit diese Methode funktioniert, muß die Komponente schon dargestellt sein. Im Falle von Applets ist dies einfach. Wir erzeugen das Image in init(). Danach wird auf das Image gezeichnet, und das Image wird auf den Schirm kopiert. Dieses Kopieren kann mit der Methode drawImage() durchgeführt werden. Die Methode hat vier Parameter, neben dem Image und den Zielkoordinaten auch einen sogenannten ImageObserver. Ein solcher Observer überwacht den Kopiervorgang. In unserem Falle geht das Kopieren so schnell, daß wir den Paramter einfach vernachlässigen. Jede Komponente kann als ImageObserver verwendet werden, und wir verwenden das gerade vorhandende Applet. import java.awt.*; import java.applet.*; public class BallApplet extends Applet implements Runnable { Thread A; boolean Stop; Image I; // Puffer Graphics G; // Graphics fuer den Puffer int X,Y,DX,DY, // Ballkoordinaten von links unten gesehen W,H; // Hoehe und Weite des Applets final int S=20; // Kugelgroesse als Konstante public void init () { W=size().width; H=size().height; 14 Threads und Images // Bestimme Hoehe und Weite I=createImage(W,H); // Erzeuge Puffer G=I.getGraphics(); paintBall(G); } public void paintBall (Graphics g) // Hintergrund und Kugel neu Zeichnen { g.setColor(Color.blue.darker()); g.fillRect(0,0,W,H); g.setColor(Color.green.darker()); g.fillOval(X-S,H-Y-S,2*S,2*S); } public void paint (Graphics g) // Kopiere einfach das Image auf den Schirm { g.drawImage(I,0,0,this); } public void update (Graphics g) // Hintergrund wird selbst neu gezeichnet // normalerweise erledigt dies update() { paint(g); } public void start () // Kugel startet in der Mitte unten { X=W/2; Y=S; DX=1; DY=15; A=new Thread(this); A.start(); Stop=false; } public void stop () // Stoppe Thread { Stop=true; } public void run () // Die Schleife { while (!Stop) { // Ball verschieben: if (X+DX<S) DX=-DX; if (X>W-S-1) DX=-DX; if (Y+DY<S) { DY=15; } X+=DX; Y+=DY; DY--; // Ball neu zeichnen und auf Schirm kopieren paintBall(G); Graphics g=getGraphics(); paint(g); g.dispose(); // Etwas verzögern try { A.sleep(50); } catch (Exception e) {} } } } 14 - 7 14 Threads und Images 14 - 8 Allerdings haben wir hier noch einige Verbesserungen vor uns, die wir im folgenden erklären. Images Laden Der Hintergrund im obigen Bild wurde mit einem Malprogramm (PC Paintbrush) erzeugt und auf eine Datei im GIF-Format abgespeichert (ziegel.gif). Sie können diese Datei von unserem Server herunterladen. Java kann außerdem noch mit dem JPEG-Format (Dateiendung jpg) umgehen, das eine bessere Kompression mit nur leichten Verlusten ermöglicht. Das GIF-Format komprimiert auf Wunsch auch, jedoch verlustfrei. Ein weiterer Unterschied ist, daß das GIF-Format mit höchstens 256 Farben arbeitet. Das Laden eines Images von einer Datei ist in einem Applet sehr einfach. Dazu muß nur eine URL und ein dazu relativer Name angegeben werden. In unserem Fall verwenden wir einfach die URL, von der das Applet geladen wurde. Beachten Sie, daß Applets nur auf Dateien zugreifen können, die auf demselben Server wie das Applet liegen. Das Kommando zum Laden des Image ist einfach Image Back=getImage(getCodeBase(),"ziegel.gif"); Dieses Image kann nun immer in den Puffer gezeichnet werden, um den Puffer zu löschen. Es wird wie üblich auf den Puffer kopiert. Im obigen Programm sieht das so aus: g.drawImage(Back,0,0,this); Man beachte, daß das Laden des Images eine Weile dauern kann, insbesondere, wenn das Laden über das Netz geschieht. Überdies wird das Laden irgendwann gestartet, spätestens dann, wenn das Image wirklich benötigt wird und geschieht asynchron in einem separaten Prozess. Es ist daher bisweilen ratsam, das Laden zu überwachen und bis zum kompletten Bild einen einfarbigen Ersatz zu zeigen. Insbesondere ist dies dann erforderlich, wenn die Größe des Bildes benötigt wird. Dazu verwendet man einen MediaTracker. Das Bild wird dann dem Tracker zur Überwachung des Ladevorganges anvertraut: MediaTracker T=new MediaTracker(this); T.addImage(Back,0); try { T.waitForAll(); } catch (InterruptedException e) {} Man kann jedem Image, das der Tracker überwacht, einen Index mitgeben. Die Methode waitForAll() wartet, bis alle Images geladen sind. Alternativ kann man auch mit checkID(0) nachfragen, ob das Bild 0 schon geladen ist. Man sollte in jedem Fall mit isErrorID(0) überprüfen, ob das Bild korrekt geladen wurde. Images Erzeugen Der Ball im obigen Beispiel ist ein teilweise transparentes Image, dessen Bild Punkt für Punkt gerendert wurde. Dazu dient die Klasse MemoryImageSource aus dem Paket java.awt.image. Ein Objekt dieser Klasse kann an createImage übergeben werden, um das Image zu erzeugen (in diesem Fall das Image Ball ). Ein MemoryImageSource wird aus einzelnen 14 Threads und Images 14 - 9 Pixeln erzeugt, die in einem int-Array gespeichert sind. Jedes Pixel besteht aus vier Bytes , die die Transparenz und die Intensität der drei Grundfarben (RGB) für das Pixel angeben (jeweils 0 bis 255, wobei 255 für "nicht transparent" steht). Man setzt das Pixel am besten mit Hilfe der Schiebeoperationen und eines bitweisen Oder (| ) zusammen. Interessant ist noch, wie die Farbanteile für die Pixel bestimmt werden. Dazu wird zunächst die zweidimensionale Aufsichtkoordinate (i,j) der Kugel in die dreidimensionale Oberflächenkoordinate (x,y,z) umgerechnet. Diese wird mit (1,1,1) skalar multipliziert. Der enstandene Wert dient, richtig skaliert, als Helligkeit an dieser Stelle. Details kann man der Funktion createBall() im folgenden Listing entnehmen. import java.awt.*; import java.applet.*; import java.awt.image.*; public class BallApplet extends Applet implements Runnable { Thread A; boolean Stop; Image I; // Image Back; Image Ball; Graphics G; Puffer // Hintergrund // Ball // Graphics fuer den Puffer int X,Y,DX,DY, // Ballkoordinaten von links unten gesehen W,H; // Hoehe und Weite des Applets final int S=20; // Kugelgroesse als Konstante public void init () { W=size().width; H=size().height; // Bestimme Hoehe und Weite I=createImage(W,H); // Erzeuge Puffer G=I.getGraphics(); Back=getImage(getCodeBase(),"ziegel.gif"); MediaTracker T=new MediaTracker(this); T.addImage(Back,0); try { T.waitForAll(); } catch (InterruptedException e) {} Ball=createBall(S); paintBall(G); } Image createBall (int S) // Ein gerenderter Ball mit Radius S { int i,j,k; int P[]=new int[4*(S+1)*(S+1)]; // fuer die Pixel k=0; double red,green,blue,light,x,y,z; for (i=-S; i<=S; i++) for (j=-S; j<=S; j++) { // Berechne x,y,z-Koordinate auf der Balloberflaeche x=-(double)i/S; y=(double)j/S; 14 Threads und Images 14 - 10 z=1-x*x-y*y; if (z<=0) P[k]=0; // außerhalb des Balls! Transparenter Punkt. else { z=Math.sqrt(z); light=(x+y+z)/Math.sqrt(3)*0.4; // Vectorprodukt mit 1,1,1 red=0.6*(1+light); // Rotanteil green=0.2*(1+light); // Gruenanteil blue=0; // Blauanteil P[k]=255<<24| // nicht transparent! (int)(red*255)<<16| (int)(green*255)<<8| (int)(blue*255); // P[k] setzt sich in vier Bytes aus den // Farben und der Transparenz zusammen } k++; } return createImage( // Erzeuge das Image new MemoryImageSource(2*S+1,2*S+1,ColorModel.getRGBdefault(), P,0,2*S+1)); } public void paintBall (Graphics g) // Hintergrund und Kugel neu Zeichnen { g.drawImage(Back,0,0,this); g.drawImage(Ball,X-S,H-Y-S,this); } public void paint (Graphics g) // Kopiere einfach das Image auf den Schirm { g.drawImage(I,0,0,this); } public void update (Graphics g) // Hintergrund wird selbst neu gezeichnet // normalerweise erledigt dies update() { paint(g); } public void start () // Kugel startet in der Mitte unten { X=W/2; Y=S; DX=1; DY=15; A=new Thread(this); A.start(); Stop=false; } public void stop () // Stoppe Thread { Stop=true; } public void run () // Die Schleife { while (!Stop) 14 Threads und Images { 14 - 11 // Ball verschieben: if (X+DX<S) DX=-DX; if (X>W-S-1) DX=-DX; if (Y+DY<S) { DY=15; } X+=DX; Y+=DY; DY--; // Ball neu zeichnen und auf Schirm kopieren paintBall(G); Graphics g=getGraphics(); paint(g); g.dispose(); // Etwas verzögern try { A.sleep(50); } catch (Exception e) {} } } } Natürlich ist es auch möglich, ein Image in RGB-Werte zu zerlegen. Der folgende Code etwa dreht ein Image um 90 Grad. static Image turn (Image I, Component c) { int W=I.getWidth(c),H=I.getHeight(c); int P[]=new int [W*H]; PixelGrabber pg=new PixelGrabber(I,0,0,W,H,P,0,W); try { pg.grabPixels(); } catch (Exception e) { return I; } int Q[]=new int[W*H]; int i,j; for (i=0; i<H; i++) for (j=0; j<W; j++) { Q[i*W+j]=P[i*W+j]; } return Toolkit.getDefaultToolkit().createImage( new MemoryImageSource(H,W,ColorModel.getRGBdefault(), Q,0,H)); } Natürlich sind auch andere Manipulationen denkbar. So könnte man Teile des Bildes transparent machen (abhängig etwa von einer Farbe), oder regelrechte Bildverarbeitung (AntiAliasing, Kontrastreduzierung etc.) betreiben. Übungsaufgaben 1. Modifizieren Sie den springenden Ball so, daß mehrere Bälle auf dem Schirm springen. Kollisionen brauchen nicht berücksichtigt zu werden. 2. Modifizieren Sie die Uhr so, daß sie nur die Zeit anzeigt. Dies soll in der Form hh:mm:ss geschehen. Verwenden Sie einfach die Methoden getHours(), getMinutes() und getSeconds() von Date. 3. Schreiben Sie 2 um, so daß eine einfache Uhr mit Zeigern angezeigt wird. 4. Erzeugen Sie ein Runnable-Objekt mit einer int-Variablen n. Lassen Sie zwei Threads diese Variable hochzählen (mit Ausgabe auf dem Schirm), bis sie einen Wert überschreitet. Beobachten Sie, ob die Ausgabe chonologisch richtig ist. Gibt es vielleicht sogar doppelte Ausgaben? Sind solche doppelte Ausgaben denkbar? 14 Threads und Images 14 - 12 5. Schreiben Sie ein Applet, daß einen Text von links nach rechts laufen läßt. Wählen Sie einen Font mit 2/3 der Applethöhe und eine Applethöhe von ca 50. Die Animation soll ähnlich wie bei den Bällen ablaufen und gepuffert sein. Lösungen. Aufgaben ohne Lösung 1. Testen Sie in Aufgabe 4. auf Doubletten, indem Sie ein Array von boolean anlegen, dessen n-te Stelle auf wahr gesetzt wird, wenn die Zahl n getroffen wurde. 2. Fügen Sie in die Klasse eine Methode inc() ein, die den alten Wert von n zurückgibt und n erhöht. Versehen Sie diese Methode mit dem Schlüsselwort synchronized (gleich hinter dem Schlüsselwort public ). Treten bei Verwendung dieser Methode immer noch Doubletten auf?