Posts mit dem Label Multitheading werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Multitheading werden angezeigt. Alle Posts anzeigen

Donnerstag, 16. Juli 2009

Multithreading: Selbst Threads starten

Wenn man Gerüchte über Generics hört, dann heißt es meistens, dass es eine ganz komplizierte Angelegenheit ist. Ich weiß gar nicht, wer auf die Idee gekommen ist.
Bei Generics geht es darum, Einen Klassennamen durch eine Variable zu ersetzen. Wer sich darunter schon etwas vorstellen kann, gut. Ansonsten macht es auch nichts.
Stell dir folgende Situation vor: Du willst die Objekte speichern, die im Laufe deines Programms erzeugt werden:


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public void addObject(Object o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

//size++ wird hier als Ausdruck und nicht als Anweisung benutzt.
//Der Wert des Ausdrucks ist der alte Wert
//Auf ähnliche Weise ist ++size der Wert nach der Erhöhung
objects[size++] = o;
}

//hier gleich ein Kleiner Test
//Die Objekte werden danach noch verwendet, deswegen werden sie in Variablen gespeichert
public static void main(String[] args) {
ObjectList l = new ObjectList();

String s = "hallo";
l.addObject(s);
System.out.println(s.length());

Date d = new Date();
l.addObject(d);
System.out.println(d.getTime());
}
}

Vielleicht findest du die Main-Methode genau so mühsam wie ich. Netto will ich ja von beiden Objekten nur eine Sache: einmal die Länge, und einmal die Millisekunden. Dazu würde auch "hallo".length() oder new Date().getTime() genügen. Eine lokale Variable wäre unnötig.
Und was kann man dafür tun? Zum Beispiel den Parameter in der gleichen Methode wieder zurückgeben:


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public Object addObject(Object o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

objects[size++] = o;
return o;
}

public static void main(String[] args) {
ObjectList l = new ObjectList();

System.out.println(((String) l.addObject("hallo")).length());
System.out.println(((Date) l.addObject(new Date())).getTime());
}
}

Das ist Ehrlich gesagt auch nicht meines. Ich weiß ja eigentlich ganz genau, was raus kommt, nur der Compiler weiß es nicht.
Wie bringt man es dem Compiler bei? Da gibt es natürlich den hässlichen Weg: Die Methode für die Relevanten Typen überladen...


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public Object addObject(Object o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

objects[size++] = o;
return o;
}

public String addObject(String s) {
return (String) addObject((Object) s);
}

public Date addObject(Date d) {
return (Date) addObject((Object) d);
}

public static void main(String[] args) {
ObjectList l = new ObjectList();

System.out.println(l.addObject("hallo").length());
System.out.println(l.addObject(new Date()).getTime());
}
}

Was ein relevanter Typ ist, kann sich ja ändern. Und deshalb gibt es, angefangen mit Java 5, dieses schöne neue Feature Namens Generics.
Wenn man Generics benutzt, wird der Typ sozusagen nicht in der Methodendeklaration sondern beim Aufruf deklariert. (Dabei meine ich allerdings nicht, während das Programm läuft, sondern wenn der Aufruf kompiliert wird. Die Feinheiten gibts vielleicht in einem späteren Post!)
Hier erst einmal der schöne Code:


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public T addObject(T o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

objects[size++] = o;
return o;
}

public static void main(String[] args) {
ObjectList l = new ObjectList();

System.out.println(l.addObject("hallo").length());
System.out.println(l.addObject(new Date()).getTime());
}
}

T ist hierbei eine Typenvariable und wird in spitzen Klammern deklariert. Der "Wert" der Variable kommt beim Aufruf zustande, indem der Typ des Parameters genommen wird. In weiterer Folge hat T in der ganzen Methode den gleichen Wert und kann ohne Casts verwendet werden.
Das heißt, wenn addObject mit einem String als Parameter aufgerufen wird, liefert es auch einen String zurück.

Mittwoch, 8. Juli 2009

Multithreading: Threads in grafischen Oberflächen

Sehen wir uns einmal kurz ein Programm an, das du ganz am Anfang in Java geschrieben haben könntest: Mit der Konsole eine Grundrechenoperation auf zwei positive ganze Zahlen anwenden.

package post06;


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;


public class Grundrechnung {
public static void main(String[] args) throws IOException {
System.out.print("Gib eine Rechnung ein: ");
//Liest eine einzeilige Eingabe von der Konsole
String s = new BufferedReader(new InputStreamReader(System.in)).readLine();
//Der reguläre Ausdruck [*+/-] entspricht dem einmaligen Auftauchen eines der
//in eckigen Klammern aufgelisteten Zeichen. Das Bedeutet, dass split die beiden
//Zahlen, aber nicht den Operator zurückgibt. Wegen "-" sind negative Zahlen
//nicht möglich.
String[] parts = s.split("[*+/-]");
if(parts.length != 2) {
System.err.println("Rechnung falsch");
return;
}
//Den Operator erhält man, in dem man das Zeichen direkt nach der ersten Zahl nimmt.
char operator = s.charAt(parts[0].length());

int zahl1 = Integer.parseInt(parts[0]);
int zahl2 = Integer.parseInt(parts[1]);

switch(operator) {
case '+':
System.out.println("Das Ergebnis: " + (zahl1 + zahl2));
break;
case '-':
System.out.println("Das Ergebnis: " + (zahl1 - zahl2));
break;
case '*':
System.out.println("Das Ergebnis: " + (zahl1 * zahl2));
break;
case '/':
if(zahl2 == 0) System.err.println("Division durch 0!");
else System.out.println("Das Ergebnis: " + (zahl1 / zahl2));
break;
default:
System.err.println("Unzulässiger Operator");
break;
}
}
}
Der Ablauf ist hier ganz klar, in der Reihenfolge, in der die Anweisungen im Sourcecode auftauchen. Jetzt nehmen wir das GUI-Beispiel vom letzten mal.
Übersichtlich ist es auch: Wenn man auf den Button drückt, wird das Element hinzugefügt. Allerdings ist aus dem von uns geschriebenen Code nicht ersichtlich, wie dieses wenn zustande kommt.

Sicherlich hast du auch schon eine Fehlermeldung in so einem Programm gesehen:
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
at post06.EditListener.actionPerformed(EditListener.java:25)
at javax.swing.JTextField.fireActionPerformed(JTextField.java:492)
at javax.swing.JTextField.postActionEvent(JTextField.java:705)
at javax.swing.JTextField$NotifyAction.actionPerformed(JTextField.java:820)
at javax.swing.SwingUtilities.notifyAction(SwingUtilities.java:1636)
at javax.swing.JComponent.processKeyBinding(JComponent.java:2849)
at javax.swing.JComponent.processKeyBindings(JComponent.java:2884)
at javax.swing.JComponent.processKeyEvent(JComponent.java:2812)
at java.awt.Component.processEvent(Component.java:5993)
at java.awt.Container.processEvent(Container.java:2041)
at java.awt.Component.dispatchEventImpl(Component.java:4583)
at java.awt.Container.dispatchEventImpl(Container.java:2099)
at java.awt.Component.dispatchEvent(Component.java:4413)
at java.awt.KeyboardFocusManager.redispatchEvent(KeyboardFocusManager.java:1848)
at java.awt.DefaultKeyboardFocusManager.dispatchKeyEvent(DefaultKeyboardFocusManager.java:704)
at java.awt.DefaultKeyboardFocusManager.preDispatchKeyEvent(DefaultKeyboardFocusManager.java:969)
at java.awt.DefaultKeyboardFocusManager.typeAheadAssertions(DefaultKeyboardFocusManager.java:841)
at java.awt.DefaultKeyboardFocusManager.dispatchEvent(DefaultKeyboardFocusManager.java:668)
at java.awt.Component.dispatchEventImpl(Component.java:4455)
at java.awt.Container.dispatchEventImpl(Container.java:2099)
at java.awt.Window.dispatchEventImpl(Window.java:2475)
at java.awt.Component.dispatchEvent(Component.java:4413)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:599)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:269)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:184)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:174)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:169)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:161)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:122)
Das ist die sogenannte "Stack Trace" der Exception: Die genaue Abfolge von Methodenaufrufen, die dazu geführt hat, dass die Exception aufgetreten ist.
Ganz oben steht in diesem Fall unsere Methode:
at post06.EditListener.actionPerformed(EditListener.java:25)
Das ist die Methode die die Exception geworfen hat. Direkt darunter steht die Methode, die unsere aufgerufen hat usw.

Im Gegensatz dazu einfach eine falsche Eingabe in dem neuen Beispiel ("a+1"):
Exception in thread "main" java.lang.NumberFormatException: For input string: "a"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
at java.lang.Integer.parseInt(Integer.java:447)
at java.lang.Integer.parseInt(Integer.java:497)
at post06.Grundrechnung.main(Grundrechnung.java:27)
Ich nehme mal an, dass ihr gesehen habt, worum es geht: Im oberen Fall ist der thread "AWT-EventQueue-0", im unteren "main". main bezieht sich einleuchtenderweise auf unsere main-Methode, die das Programm startet. Das sieht man auch in der Stack Trace: Die unterste Methode, die Methode, die alle anderen aufgerufen hat, ist die main-Methode.

alle anderen Methoden werden in main aufgerufen. Erst, wenn die Methode fertig ist, wird die nächste aufgerufen. Die Reihenfolge ist fix.

Aber was ist mit der anderen Stack Trace?
   at java.awt.EventDispatchThread.run(EventDispatchThread.java:122)
Die unterste Methode ist nicht die main-Methode! Das bedeutet, dass unsere GUI einen eigenen, unabhängigen Programmablauf hat:

Auch in unserer GUI-Anwendung gibt es eine main-Methode, aber alles, was in der GUI passiert (z.B. die actionPerformed-Methode unseres Listeners), passiert in einem separaten Thread: Diese Dinge können gleichzeitig und unabhängig zum main-Thread passieren.

Das war eine ziemlich lange Einführung für das Thema. Was ich zeigen wollte ist:
  • Mit mehreren Threads können mehrere Aufgaben gleichzeitig ablaufen
  • Auch, wenn du es nicht gewusst hast, hast du sicher schon lange mit Threads gearbeitet
  • Swing (mit AWT zusammen) verwendet einen eigenen Thread dafür, dass die Benutzereingaben zu deinem Programm gelangen
  • Das ist notwendig, weil Benutzereingaben großteils warten bedeutet: Warten, bis eine Taste oder ein Button gedrückt wurde. Und warten braucht Zeit.
  • Ohne eigenen Thread könntest du in deinem Programm keine Sachen im Hintergrund machen, während die GUI angezeigt wird.
Das gilt aber auch in die andere Richtung: wenn deine actionPerformed-Methode eine Aufgabe ausführt, die sehr lange benötigt (Suche im Dateisystem, Netzwerkarbeit, Grafikoperationen, ...), dann blockiert die Methode den Swing-Thread.
Innerhalb des Threads gibt es, genau so wie im main-Thread, eine fixe Reihenfolge der Methodenaufrufe. Solange deine actionPerformed-Methode nicht abläuft, kann Swing keine Events bearbeiten. Das sieht dann so aus, dass deine Anwendung nicht reagiert.

Für heute war das lang genug, ein paar Praxisbeispiele machen wir später ;)