Threads und Images

Werbung
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?
Herunterladen