Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung |
<< | < | > | >> | Kapitel 22 - Multithreading |
Nicht immer ist es möglich, eine Klasse, die als Thread laufen soll, von Thread abzuleiten. Dies ist insbesondere dann nicht möglich, wenn die Klasse Bestandteil einer Vererbungshierarchie ist, die eigentlich nichts mit Multithreading zu tun hat. Da Java keine Mehrfachvererbung kennt, kann eine bereits abgeleitete Klasse nicht von einer weiteren Klasse erben. Da sehr unterschiedliche Klassen als Thread parallel zu vorhandenem Code ausgeführt werden können, ist dies eine sehr unschöne Einschränkung des Multithreading-Konzepts von Java.
Glücklicherweise gibt es einen Ausweg. Er besteht darin, einen Thread nicht durch Ableiten aus Thread, sondern durch Implementierung des Interfaces Runnable zu erzeugen. Runnable enthält nur eine einzige Deklaration, nämlich die der Methode run:
public abstract void run() |
java.lang.Runnable |
Tatsächlich muß jede Klasse, deren Instanzen als Thread laufen sollen, das Interface Runnable implementieren (sogar die Klasse Thread selbst). Um eine nicht von Thread abgeleitete Instanz in dieser Weise als Thread laufen zu lassen, ist in folgenden Schritten vorzugehen:
Nun startet das Thread-Objekt die run-Methode des übergebenen Objekts, das sie ja durch die Übergabe im Konstruktor kennt. Da dieses Objekt das Interface Runnable implementiert, ist garantiert, daß eine geeignete Methode run zur Verfügung steht.
Wir wollen dies an einem Beispiel deutlich machen:
001 /* Listing2204.java */ 002 003 class A2204 004 { 005 int irgendwas; 006 //... 007 } 008 009 class B2204 010 extends A2204 011 implements Runnable 012 { 013 public void run() 014 { 015 int i = 0; 016 while (true) { 017 if (Thread.interrupted()) { 018 break; 019 } 020 System.out.println(i++); 021 } 022 } 023 } 024 025 public class Listing2204 026 { 027 public static void main(String[] args) 028 { 029 B2204 b = new B2204(); 030 Thread t = new Thread(b); 031 t.start(); 032 try { 033 Thread.sleep(1000); 034 } catch (InterruptedException e){ 035 //nichts 036 } 037 t.interrupt(); 038 } 039 } |
Listing2204.java |
Die Klasse B2204 ist von A2204 abgeleitet und kann daher nicht von Thread abgeleitet sein. Statt dessen implementiert sie das Interface Runnable. Um nun ein Objekt der Klasse B2204 als Thread auszuführen, wird in main von Listing2204 eine Instanz dieser Klasse erzeugt und an den Konstruktor der Klasse Thread übergeben. Nach dem Aufruf von start wird die run-Methode von B2204 aufgerufen.
Auf eine ähnliche Weise lassen sich auch Methoden, die ursprünglich nicht als Thread vorgesehen waren, in einen solchen umwandeln und im Hintergrund ausführen. Der Grundstein für die Umwandlung eines gewöhnlichen Objekts in einen Thread wird dabei immer bei der Übergabe eines Runnable-Objekts an den Konstruktor des Thread-Objekts gelegt. Das folgende Beispiel demonstriert, wie eine zeitintensive Primfaktorzerlegung im Hintergrund laufen kann.
Zunächst benötigen wir dazu eine Klasse PrimeNumberTools, die Routinen zur Berechnung von Primzahlen und zur Primfaktorzerlegung zur Verfügung stellt. Diese Klasse ist weder von Thread abgeleitet, noch implementiert sie Runnable:
001 /* PrimeNumberTools.java */ 002 003 public class PrimeNumberTools 004 { 005 public void printPrimeFactors(int num) 006 { 007 int whichprime = 1; 008 int prime; 009 String prefix; 010 011 prefix = "primeFactors("+num+")= "; 012 while (num > 1) { 013 prime = getPrime(whichprime); 014 if (num % prime == 0) { 015 System.out.print(prefix+prime); 016 prefix = " "; 017 num /= prime; 018 } else { 019 ++whichprime; 020 } 021 } 022 System.out.println(); 023 } 024 025 public int getPrime(int cnt) 026 { 027 int i = 1; 028 int ret = 2; 029 030 while (i < cnt) { 031 ++ret; 032 if (isPrime(ret)) { 033 ++i; 034 } 035 } 036 return ret; 037 } 038 039 private boolean isPrime(int num) 040 { 041 for (int i = 2; i < num; ++i) { 042 if (num % i == 0) { 043 return false; 044 } 045 } 046 return true; 047 } 048 } |
PrimeNumberTools.java |
Ohne Hintergrundverarbeitung könnte PrimeNumberTools instanziert und ihre Methoden durch einfachen Aufruf verwendet werden:
001 /* Listing2206.java */ 002 003 import java.io.*; 004 005 public class Listing2206 006 { 007 public static void main(String[] args) 008 { 009 PrimeNumberTools pt = new PrimeNumberTools(); 010 BufferedReader in = new BufferedReader( 011 new InputStreamReader( 012 new DataInputStream(System.in))); 013 int num; 014 015 try { 016 while (true) { 017 System.out.print("Bitte eine Zahl eingeben: "); 018 System.out.flush(); 019 num = (new Integer(in.readLine())).intValue(); 020 if (num == -1) { 021 break; 022 } 023 pt.printPrimeFactors(num); 024 } 025 } catch (IOException e) { 026 //nichts 027 } 028 } 029 } |
Listing2206.java |
Das Programm erzeugt eine Instanz der Klasse PrimeNumberTools und führt für jeden eingelesenen Zahlenwert durch Aufruf der Methode printPrimeFactors die Primfaktorzerlegung durch. Um nun diese Berechnungen asynchron durchzuführen, entwerfen wir eine Wrapper-Klasse, die von PrimeNumberTools abgeleitet wird und das Interface Runnable implementiert:
001 /* ThreadedPrimeNumberTools.java */ 002 003 public class ThreadedPrimeNumberTools 004 extends PrimeNumberTools 005 implements Runnable 006 { 007 private int arg; 008 private int func; 009 010 public void printPrimeFactors(int num) 011 { 012 execAsynchron(1,num); 013 } 014 015 public void printPrime(int cnt) 016 { 017 execAsynchron(2,cnt); 018 } 019 020 public void run() 021 { 022 if (func == 1) { 023 super.printPrimeFactors(arg); 024 } else if (func == 2) { 025 int result = super.getPrime(arg); 026 System.out.println("prime number #"+arg+" is: "+result); 027 } 028 } 029 030 private void execAsynchron(int func, int arg) 031 { 032 Thread t = new Thread(this); 033 this.func = func; 034 this.arg = arg; 035 t.start(); 036 } 037 } |
ThreadedPrimeNumberTools.java |
Hier wurde die Methode printPrimeFactors überlagert, um den Aufruf der Superklasse asynchron ausführen zu können. Dazu wird in execAsynchron ein neuer Thread generiert, dem im Konstruktor das aktuelle Objekt übergeben wird. Durch Aufruf der Methode start wird der Thread gestartet und die run-Methode des aktuellen Objekts aufgerufen. Diese führt die gewünschten Aufrufe der Superklasse aus und schreibt die Ergebnisse auf den Bildschirm. So ist es möglich, bereits während der Berechnung der Primfaktoren einer Zahl eine neue Eingabe zu erledigen und eine neue Primfaktorberechnung zu beginnen.
Um dies zu erreichen, ist in der Klasse Listing2206 lediglich die Deklaration des Objekts vom Typ PrimeNumberTools durch eine Deklaration vom Typ der daraus abgeleiteten Klasse ThreadedPrimeNumberTools zu ersetzen:
001 /* Listing2208.java */ 002 003 import java.io.*; 004 005 public class Listing2208 006 { 007 public static void main(String[] args) 008 { 009 ThreadedPrimeNumberTools pt; 010 BufferedReader in = new BufferedReader( 011 new InputStreamReader( 012 new DataInputStream(System.in))); 013 int num; 014 015 try { 016 while (true) { 017 System.out.print("Bitte eine Zahl eingeben: "); 018 System.out.flush(); 019 num = (new Integer(in.readLine())).intValue(); 020 if (num == -1) { 021 break; 022 } 023 pt = new ThreadedPrimeNumberTools(); 024 pt.printPrimeFactors(num); 025 } 026 } catch (IOException e) { 027 //nichts 028 } 029 } 030 } |
Listing2208.java |
Wenn alle Eingaben erfolgen, bevor das erste Ergebnis ausgegeben wird,
könnte eine Beispielsitzung etwa so aussehen (Benutzereingaben
sind fett gedruckt):
Bitte eine Zahl eingeben: 991
Bitte eine Zahl eingeben: 577
Bitte eine Zahl eingeben: 677
Bitte eine Zahl eingeben: -1
primeFactors(577)= 577
primeFactors(677)= 677
primeFactors(991)= 991
Obwohl das gewünschte Verhalten (nämlich die asynchrone Ausführung einer zeitaufwendigen Berechnung im Hintergrund) realisiert wird, ist dieses Beispiel nicht beliebig zu verallgemeinern. Die Ausgabe erfolgt beispielsweise nur dann ohne Unterbrechung durch Benutzereingaben, wenn alle Eingaben vor der ersten Ausgabe abgeschlossen sind. Selbst in diesem Fall funktioniert das Programm nicht immer zuverlässig. Es ist generell problematisch, Hintergrundprozessen zu erlauben, auf die Standardein- oder -ausgabe zuzugreifen, die ja vorwiegend vom Vordergrund-Thread verwendet wird. Ein- und Ausgaben könnten durcheinander geraten und es könnte zu Synchronisationsproblemen kommen, die die Ausgabe verfälschen. Wir haben nur ausnahmsweise davon Gebrauch gemacht, um das Prinzip der Hintergrundverarbeitung an einem einfachen Beispiel darzustellen. |
|
Das nächste Problem ist die Realisierung des Dispatchers in run, der mit Hilfe der Instanzvariablen func und arg die erforderlichen Funktionsaufrufe durchführt. Dies funktioniert hier recht problemlos, weil alle Methoden dieselbe Parametrisierung haben. Im allgemeinen wäre hier ein aufwendigerer Übergabemechanismus erforderlich.
Des weiteren sind meistens Vorder- und Hintergrundverarbeitung zu synchronisieren, weil der Vordergrundprozeß die Ergebnisse des Hintergrundprozesses benötigt. Auch hier haben wir stark vereinfacht, indem die Ergebnisse einfach direkt nach der Verfügbarkeit vom Hintergrundprozeß auf den Bildschirm geschrieben wurden. Das Beispiel zeigt jedoch, wie prinzipiell vorgegangen werden könnte, und ist vorwiegend als Anregung für eigene Experimente anzusehen.
Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0 |
<< | < | > | >> | © 2000 Guido Krüger, http://www.gkrueger.com |