Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
Java Multithread Support - Asynchron ausführbare Tätigkeiten (Callable & Future)

Java Multithread Support - Asynchron ausführbare Tätigkeiten (Callable & Future)
Java Multithread Support
Asynchron ausführbare Tätigkeiten (Callable & Future)

JavaSPEKTRUM, März 2005
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im JavaSPEKTRUM erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

 
 

Wir haben uns bereits in der letzten Ausgabe dieser Kolumne (siehe / KRE7 /) einen Überblick über Neuerungen im JDK 5.0 angesehen, die für die Multithread-Programmierung zur Verfügung stehen.  Darunter sind auch Abstraktionen, die das Ausführen von Tätigkeiten in Threads unterstützen.  Am spannendsten ist dabei sicher der Threadpool, mit dessen Hilfe sich Tätigkeiten auf bereitstehende Threads verteilen und sogar ggf. in einer Taskqueue speichern lassen.  Der Threadpool verwendet dabei aber nicht nur das allseits bekannte Runnable zur Beschreibung einer asynchron ausführbaren Tätigkeit, sondern arbeitet auch mit den in Java 5.0 neuen Abstraktionen Callable und Future.  In diesem Beitrag sehen wir uns, zur Vorbereitung auf den nächsten Beitrag über Threadpools, die Interfaces Callable und Future an.
 

Ausführen von Threads

Abbildung 1 zeigt die Interfaces und Klassen, die für das Thema „Ausführen von Threads“ relevant sind.  Dazu gehören nicht nur die Threadpools, sondern auch Callable, Future und anverwandte Abstraktionen.


Abbildung 1:Klassen und Interfaces zum Ausführen von Threads





Entsprechend der UML sind die Namen von Interfaces kursiv. Genauso wird bei der Vererbung zwischen extends-Beziehungen (Pfeil mit durchgezogener Linie) und implements-Beziehungen (Pfeil mit gestrichelter Linie) unterschieden.

Das Diagramm ist relativ dicht gedrängt. Damit wird deutlich, wie umfangreich das Thema nun geworden ist. Bisher gab es an dieser Stelle ja nur das Interface Runnable. Alle anderen Abstraktionen sind mit dem JDK 5.0 neu dazugekommen. Eine weitere Neuerung des JDK 5.0, nämlich die Generics, werden bereits bei der Definition der neuen Abstraktionen genutzt. Future<V> ist zum Beispiel keine einfaches Interface, sondern ein generisches Interface mit einem Typparameter V.

Beginnen wir die Diskussion der neuen Abstraktionen mit den beiden neuen zentralen Interfaces Callable<V> und Future<V>.
 

Callable<V>

Vielleicht haben Sie sich auch schon einmal eine Alternative zu dem Runnable Interface gewünscht, die es erlaubt, Ergebnisse bzw. Exceptions zurückzuliefern. Mit dem JDK 5.0 gibt es nun eine solche Alternative: das generische Interface Callable<V>.

Callable<V> ist ein Interface, das parallel zum bereits seit JDK 1.0 existierenden Runnable eingeführt wurde. Beide Interfaces erlauben es, eine definierte Funktionalität asynchron ausführen zu lassen. Wie wohl allgemein bekannt ist, implementiert man dazu beim Runnable die Methode:

  public void run();

Die in run() implementierte Funktionalität kann dann asynchron in einem Thread ausgeführt werden. Beim einem Callable<V> ist es ganz ähnlich.  Es wird die folgende Methode implementiert:

  public V call() throws Exception;

Dabei ist der Returntype V der Methode call() der Typparameter des generischen Interfaces Callable<V>.   Mit diesem Typparameter ist es möglich,  von einer asynchronen Funktionalität, die in call() implementiert wurde, ein Ergebnis eines beliebigen, aber zur Compilezeit festen, Referenztyps zurückzubekommen. Falls auf Grund eines Fehlers kein Ergebnis ermittelt werden kann, ist es möglich, eine Exception zu werfen. Das heißt, Callable<V> erlaubt es nun explizit, ein Ergebnis bzw. einen Fehler von einer asynchron ausgeführten Tätigkeit zu bekommen. Mit Runnable war das so nicht direkt möglich.

Hier ist ein Beispiel für die Implementierung eines Callable, das die Anzahl der Millisekunden zurückliefert, die benötigt werden, um tausendmal "Hello World!" auszudrucken.

public class MyCallable implements Callable<Long> {
   public Long call()  {
      long t = System.currentTimeMillis();

      for (int I=0; I<1000; i++)
         System.out.println("Hello World!");

      return ( System.currentTimeMillis() – t);
   }
}

Ein Problem ist jetzt natürlich:  wie kann man ein Callable als eigenen Thread starten?  Die Schnittstelle der Klasse Thread hat sich mit dem JDK 5.0 nicht so geändert, dass jetzt alternativ ein Thread mit einem Callable gestartet werden kann. Hier wird weiterhin ein Runnable erwartet.  Es wäre genau betrachtet auch gar nicht sinnvoll gewesen, einem Thread ein Callable statt einem Runnable zu geben. Man hätte dann noch einen Mechanismus benötigt, um Ergebnisse oder Fehler des asynchron in einem eigenen Thread ablaufenden Callables zu ermitteln.

Nachfolgend wollen wir uns den Adapter ansehen, der aus einem Callable<V> ein Runnable macht, das man einem Thread übergeben und starten kann. Aber vorher sehen wir uns erst einmal das Future-Pattern an.  Die Adapterklasse FutureTask ist nämlich nicht nur ein Adapter, sondern bietet auch Unterstützung beim Abholen des Resultats eines Callable<V> - und darum geht es genau beim Future-Pattern.

Abbildung 2 zeigt die alle Klassen und Interfaces, die im Zusammenhang mit Runnable und Callable relevant sind.


Abbildung 2: Klassen und Interfaces zur Beschreibung von asynchron ausführbaren Tätigkeiten

Future<V>

Häufig will man erfahren, dass eine asynchrone Tätigkeit, die parallel zur eigentlichen Anwendung ausgeführt wird, fertig geworden ist, um ihr Ergebnis auszuwerten. Stellen Sie sich dazu zum Beispiel eine mit Swing implementierte GUI-Anwendung vor, die unter anderem die Funktionalität hat, Dateien von entfernten Rechnern herunterzuladen. Das Herunterladen geschieht dabei als asynchrone Tätigkeit in einem eigenen Thread parallel zur Anwendung. So ist die Anwendung auch während des Herunterladens in der Lage, Benutzerinput zu verarbeiten. Nach dem Herunterladen wird das erfolgreiche Beenden der Aktion zusammen mit dem Dateinamen, der Größe der Datei und der durchschnittlichen Geschwindigkeit angezeigt. Kommt es zu einem Fehler beim Herunterladen, so wird dieser stattdessen zusammen mit dem Dateinamen angezeigt. Das typische Problem ist nun: wie meldet sich der Thread, der das Herunterladen durchführt hat, am Ende zurück und teilt sein Ergebnis mit? Dafür gibt es zwei grundsätzlich unterschiedliche Lösungsansätze:
  • Callback
  • Future-Pattern
Beim Callback wird der Thread, der das Herunterladen durchführt, am Ende die Methode eines Callback-Interfaces aufrufen, um das Ergebnis mitzuteilen. Typischerweise wird die Anwendung beim Start des Herunterladens eine Instanz einer konkreten Implementierung dieses Interfaces als Parameter mitgeben. Das Problem mit der Callback-Lösung ist, dass der Callback-Code unter Kontrolle des Threads ausgeführt wird, der die asynchrone Hilfstätigkeit ausgeführt hat. D.h. eine Synchronisation mit dem Haupt-Thread muss immer noch erfolgen und dazu muss der Haupt-Thread irgendwelche Mechanismen zur Synchronisation anbieten. In unserem konkreten Beispiel ist dies kein Problem, weil Swing so etwas vorsieht. Zum Beispiel könnte am Ende unseres Callbacks mit SwingUtilities.inovkeLater() eine solche Synchronisation erfolgen. Aber im  allgemeinen ist die Callback-Technik nicht ganz unkompliziert.

Wie funktioniert nun das Future-Pattern, dem eigentlich unsere Hauptaufmerksamkeit gilt? Hier hat man eine explizite Abstraktion, das Future, welche die Synchronisation zwischen den Threads sowie die Ergebnisübergabe übernimmt. Im generischen Interface Future<V> des JDK 5.0 stehen dafür folgende Methoden zur Verfügung:

V get() throws InterruptedException, ExecutionException

  • Wartet, bis die asynchrone Tätigkeit abgeschlossen ist, und gibt dann ihr Ergebnis zurück. Dabei ist V der Typparameter des generischen Interface Future<V>. Das heißt, es wird ein Ergebnisse vom einem beliebigen Referenztypen zurückgeliefert. Falls die Tätigkeit mit interrupt() abgebrochen wurde, wird die InterruptedException geworfen. Falls die Ausführung der Tätigkeit mit einer Exception abgebrochen wurde, wird die ExecutionException geworfen. Sie enthält die Orginal-Exception, die zum Abbruch geführt hat.
V get(long timeout, TimeUnit granularity) throws InterruptedException, ExecutionException, TimeoutException
  • Wartet die mit timeout und granularity definierte Zeitspanne ab, ob die asynchrone Tätigkeit beendet wird. Wenn ja, wird das Ergebnis zurückgeliefert. Wenn nein, wird die TimeoutException geworfen. Für die anderen Exception gilt das oben Gesagte.
boolean isDone()
  • Gibt true zurück, falls die asynchrone Tätigkeit abgeschlossen ist.
boolean isCancelled()
  • Gibt true zurück, falls die asynchrone Tätigkeit abgebrochen wurde, ehe sie abgeschlossen werden konnte.
boolean cancel(Boolean mayInterruptIfRunning)
  • Unternimmt den Versuch, die Ausführung der asynchronen Tätigkeit zu unterbrechen.  Dieser Versuch schlägt natürlich fehl, wenn die Tätigkeit bereits abgeschlossen ist oder vorher schon abgebrochen wurde.  Wenn das Abbrechen möglich ist und die Tätigkeit noch nicht begonnen hat, dann wird die Tätigkeit gar nicht erst gestartet. Wenn die Tätigkeit schon läuft, dann wird abhängig vom Parameter mayInterruptIfRunning entschieden, ob das Abbrechen versucht wird.  Versucht wird das Abbrechen über den Aufruf von interrupt() auf dem ausführenden Thread.  Das heißt, ob sich die Tätigkeit tatsächlich abbrechen läßt, hängt davon ab, ob die Tätigkeit auf den Interrupt-Status des ausführenden Threads reagiert.  (Wie das mit dem Interrupt funktioniert, haben wir in / KRE6 / beschrieben.)  Wenn sich die Tätigkeit nicht abbrechen ließ, dann wird false zurückgegeben, sonst true.
Diese Future Interface vereinigt sozusagen die Funktionalität des Future-Patterns, d.h. das Warten auf das Ergebnis, mit der Funktionalität, die Ergebnisbeschaffung abzubrechen. Zusätzlich kann man mit isDone und isCancelled Informationen über den Beendigungszustand der Tätigkeit einzuholen.  (In frühen, experimentellen Versionen des Packages java.util.concurrent waren diese Funktionalitäten getrennt und über zwei verschiedene Interfaces beschrieben: ein Future Interface, das nur das Warten und Abholen des Ergebnisses definierte, und ein Cancellable Interface, das für das Abrechen der Tätigkeit zuständig war.  In der endgültigen Version im JDK 1.5 sind nun beide Funktionalitäten im Future Interface vereint.)

Zum Interface Future<V> gibt eine Klasse in der Multithread-Support-Erweiterung des JDK 5.0, nämlich die FutureTask<V>, die die Funktionalität des Future Interfaces implementiert.

FutureTask<V>

FutureTask<V> ist eine wichtige Klasse der Multithread-Erweiterungen des JDK 5.0. Sie implementiert die beiden zentralen Interfaces Runnable und Future<V>.  Vorstellen kann man sich die FutureTask als einen intelligente Wrapper für ein Runnable oder ein Callable.  Einerseits implementiert die FutureTask die run() Funktionalität für die Ausführung einer asynchronen Tätigkeit, basierend auf dem unterliegenden Runnable oder Callable.  Zusätzlich implementiert die FutureTask noch all die Methoden des Future, indem sie Ergebnisse und Exceptions von der asynchronen Tätigkeit aufsammelt, ggf. aufbewahrt und auf Abruf abliefert oder darauf wartet, falls nötig.  Zusätzlich werden noch die Zustände der Tätigkeit („fertig“ / „abgebrochen“ / „noch nicht angefangen“) verwaltet und der Abbruch der Tätigkeit ermöglicht.

Konstruieren läßt sich eine FutureTask<V> entweder mit einem Callable<V>, dessen Ergebnis sie beim Future<V>.get() zurückliefert, oder mit einem Runnable und einem vordefinierten Ergebniswert beliebigen Referenztyps, welcher beim Future<T>.get() zurückliefert wird.

FutureTask(Callable<V> callable)
FutureTask(Runnable runnable, V result)

Bekanntlich erzeugt ein Runnable gar kein Ergebnis, die FutureTask hat aber eine get() Methode, die ein Ergebnis zurückliefert.  Damit die FutureTask später beim Abholen des „Ergebnisses“ etwas abliefern kann, muß bei der Konstruktion dieses „Ergebnis“ bereits mitgegeben werden.  Dieser Wert, den man bei der Konstruktion der FutureTask zu dem Runnable dazugibt, ist sozusagen ein Dummy-Ergebnis. Wenn das durchgeschleuste Objekt gar nicht gebraucht wird, was häufig vorkommt, dann übergibt man üblicherweise Boolean.TRUE als Dummy.

Mit der Klasse FutureTask<V> lassen sich also Runnables und Callable<V>s in Runnables verpacken und als asynchrone Tätigkeiten starten, indem sie dem Konstruktor von Thread übergeben werden und der Thread danach gestartet wird. Man kann auf die Beendigung der Tätigkeit, mit oder ohne Ergebnis, warten. Das macht man über das Interface Future<V>.  Die asynchronen Tätigkeiten  lassen sich vorzeitig beenden, ebenfalls über das Interface Future<V>.  Insgesamt gesehen ist die FutureTask die zentrale Abstraktion, die das Ausführen (oder Abbrechen) einer asynchronen Tätigkeiten und das Entgegennehmen des resultierenden Ergebnisses (oder Fehlers) unterstützt.

Auch unter einem anderen Gesichtspunkt ist FutureTask<V> eine wichtige Klasse. Sie ist der Adapter, der aus einem Callable<V> ein Runnable macht. Das neu eingeführt Interface Callable<V> hat zwar gegenüber dem Runnable den Vorteil, dass jetzt auch ein Ergebnis bzw. eine Exception von einer asynchronen Tätigkeit zurückgeliefert werden kann, aber fast überall wird weiterhin ein Runnable und kein Callable<V> als Parameter erwartet. Zum Beispiel gibt es keinen neuen Konstruktor für Thread, der es erlauben würde, einen Thread mit einem Callable<V> zu konstruieren. In solchen Fällen benutzt man die FutureTask<V> als Adapter. Schauen wir uns dazu ein Beispiel an. Das folgende Callable<Integer> ist ein primitiver Benchmark, der zählt, wie häufig „Hello World!“ innerhalb von 1 sec ausdruckt werden kann, und liefert diese Anzahl als Ergebnis zurück.

public class HelloWorldCount implements Callable<Integer> {
  public Integer call() throws InterruptedException {
    long end = new Date().getTime() + 1000;

    int i;
    for (i = 0; new Date().getTime() < end; i++ ) {
      System.out.println("HelloWorld! ");
      if (Thread.interrupted())
        throw new InterruptedException();
    }
    return new Integer(i);
  }

  static public void main(String[] argv) {
    FutureTask<Integer> t = new FutureTask<Integer>(new HelloWorldCount()); // 1

    new Thread(t).start(); // 2

    try {
      System.out.println("Die Anzahl war: " + t.get());
    } catch (ExecutionException e) {
      Throwable th = e.getCause();
      if (th == null)
         System.out.println("HelloWorldCount wurde aus unbekannten Gründen abgebrochen. ");
      else if (th instanceof InterruptedException)
        System.out.println("HelloWorldCount wurde abgebrochen. ");
      else
        System.out.println("HelloWorldCount wurde mit folgendem Fehler: " + th + " abgebrochen. ");
    } catch (InterruptedException e) {
        System.out.println("HelloWorldCount wurde abgebrochen. ");
    }
  }
}

In der main-Methode wird nun unser HelloWorldCount (welches ein Callable<Integer> ist) mit Hilfe einer FutureTask<Integer> in ein Runnable adaptiert (Zeile 1) und in einem eigenen Thread als asynchrone Tätigkeit gestartet (Zeile 2). Der Rest der Methode besteht nur noch aus der Auswertung des Ergebnisses bzw. der Fehlers.

Das Beispiel zeigt u.a., wie man Exceptions, die von einer asynchronen Tätigkeit ausgelöst werden, lokal über das Future fangen und behandeln kann.  Das ist einer der Vorzüge der Callables.  Wenn man mit Runnables arbeitet, dann können diese Runnables ohnehin keine Checked Exceptions werfen, aber sie könnten noch immer Runtime Exceptions (oder Errors) auslösen.  Wenn ein Runnable so etwas tut, dann kann es passieren, dass der Thread mit einer Runtime Exception abbricht, ohne dass diese Ausnahmesituation irgendwo behandelt würde.  Was macht man dann?
 

Behandlung von ungefangenen Exceptions im Zusammenhang mit Threads

Runtime Exceptions (oder Errors), die nicht in einem Thread abgefangen werden, führen dazu, dass der Thread beendet wird. Um auf ein solches Vorkommnis zu reagieren, kann man von der Klasse ThreadGroup ableiten und in ihrer uncaughtException() Methode eine geeignete Behandlung implementieren.
Zwar ist die ThreadGroup seit dem JDK 1.0 in Java vorhanden, aber eigentlich spielt sie in der Multihread-Programmierung keine sehr wichtige Rolle. Deshalb gibt es mit dem JDK 5.0 ein neues in die Klasse Thread geschachteltes Interface: Thread.UncaughtExceptionHandler. Jede Klasse, die dieses Interface implementiert, kann nun als Handler für ungefangene Exceptions genutzt werden, die zur Beendigung eines Threads geführt haben. Dazu bietet die Klasse Thread zwei neue Methoden an:
  • Die statische Methode setDefaultUncaughtExceptionHandler() erlaubt es, einen globalen Handler vom Typ Thread.UncaughtExceptionHandler einzuhängen. Dieser Handler wird im Bedarfsfall für alle Threads genutzt.
  • Mit der Instanzmethode setUncaughtExceptionHandler() hängt man einen Handler vom Typ Thread.UncaughtExceptionHandler für den Thread ein, auf dem die Methode aufgerufen wird.
Wenn nun ein Thread von einer Runtime Exception beendet worden ist, wird je nachdem ob ein globaler und/oder ein lokaler Handler eingehängt worden ist, in der folgenden Reihenfolge genau ein Handler gesucht, der dann aufgerufen wird:
  • lokaler Handler (falls vorhanden)
  • globaler Handler (falls vorhanden)
  • ThreadGroup.uncaughtException() (immer vorhanden).


Hier ist ein Beispiel, in dem ein Uncaught-Exception-Handler für einen einzelnen Thread eingehängt wird:

class SomeRunnable implements Runnable {
  public void run() {
     …
     throw new SomeRuntimeException();
  }
}
class Test {
  private static void test1() {
     Thread th;
     Thread.UncaughtExceptionHandler eh = new Thread.UncaughtExceptionHandler() {
       public void uncaughtException(Thread t, Throwable e) {
         e.printStackTrace();
         … eigentliche Fehlerbehandlung …
       }
     };
     th = new Thread(new SomeRunnable ());
     th.setUncaughtExceptionHandler(eh);
     th.start();
  }
}

Eine solche Lösung  über einen Uncaught-Exception-Handler ist recht grobgranular.  Sie gilt entweder für einen Thread, wie im Beispiel gezeigt, oder für eine Gruppe von Threads oder alle Threads.  Man muß für das Einhängen eines Uncaught-Exception-Handlerist immer Zugriff auf den oder die Threads haben, die eine Tätigkeit ausführen. Wenn ein Thread mehrere Tätigkeiten ausführt, wie das zum Beispiel bei einem Threadpool der Fall ist, dann möchte man u.U. pro Tätigkeit eine gesonderte Fehlerbehandlung durchführen.  Das läßt sich mit einem Uncaught-Exception-Handler nicht erreichen.  Mit einem Future hingegen ist es problemlos möglich, die Runtime Exception (oder den Error) von einem Runnable ganz lokal abzuholen und zu behandeln.  Dazu muß man das Runnable in eine FutureTask verpacken; dann kann über Future.get() die Runtime Exception (oder der Error) genau dieses einen Runnables gefangen werden.  Das nachfolgende Beispiel skizziert eine solche Lösung:

class SomeRunnable implements Runnable {
  public void run() {
     …
     throw new SomeRuntimeException();
  }
}
class Test {
  private static void test() {
     Thread th;
     FutureTask<Void> f = new FutureTask<Void>(new SomeRunnable(), null);
     th = new Thread(f);
     th.start();
     try {
       f.get();
     } catch (Throwable e)  {
       e.printStackTrace();
       … eigentliche Fehlerbehandlung …
     }
  }
}

Die Lösung über das Future ist im allgemeinen die elegantere Lösung, weil sie lokal funktioniert, statt die Verantwortlichkeiten über verschiedene Teile des Programms zu verteilen.  Bei der Lösung über einen Uncaught-Exception-Handler muß man sich in die Erzeugung bzw. Konfiguration des ausführenden Threads einhängen, um etwaige Runtime Exceptions (oder Errors) zu behandeln. Das ist nicht immer möglich.  Im nächsten Artikel werden wir sehen, daß man Tätigkeiten auch einem Threadpool zur Ausführung übergeben kann.  Dann hat lediglich der Threadpool Zugriff auf die ausführenden Threads und der Benutzer sieht gar nicht, welcher Thread die betreffende Tätigkeit ausführt.  Damit hat der Benutzer auch gar keine Chance, zur Fehlerbehandlung einen Uncaught-Exception-Handler in den ausführenden Thread einzuhängen.  Dann bleibt nur der Weg, die Fehlerbehandlung an der einzelnen Tätigkeit aufzuhängen. Das ist auch sonst, unabhängig von der Verwendung eine Thread-Pools, sinnvoll, weil es meistens sowieso einen Programmteil gibt, der sich um die Beendigung und das Ergebnis der Tätigkeit kümmert.  Dieser Programmteil kann sich dann auch um das Scheitern der Tätigkeit per Runtime Exception (oder Error) kümmern.
 

Zusammenfassung

Für das Ausführen von Threads sind in Java 5.0 neue Abstraktionen zum JDK hinzugekommen.  Es gibt das ergebnisproduzierende Callable als Alternative zum ergebnislosen Runnable.  Es gibt das Future als zentrale Abstraktion für das Abholen des Resultats oder der Exceptions, die aus einer Tätigkeit entstehen.  Wie man Tätigkeiten nun in Threads zur Ausführung bringt, betrachten wir im nächsten Beitrag, der sich den Threadpools im JDK 5.0 widmet.
 
 

Literaturverweise

 
/KRE6/ Multithread Support in Java, Teil 6: Anhalten und Beenden von Threads
Klaus Kreft & Angelika Langer
JavaSPEKTRUM, November 2004
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/17.StopThread/17.StopThread.html
/KRE7/ Multithread Support in Java, Teil 7: Threadsichere Collections und Synchronizers
Klaus Kreft & Angelika Langer
JavaSPEKTRUM, Januar 2005
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/18.Synchronizers/18.Synchronizers.html
/JAVA5/  Java Standard Edition 5.0
URL: http://java.sun.com/j2se/1.5.0/

 
 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
Concurrent Java - Java Multithread Programming
4 day seminar ( open enrollment and on-site)
 
  © Copyright 1995-2008 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/19.Callables/19.Callables.html  last update: 26 Nov 2008