Java FX Concurrency – Tasks und Services

Java FX stellt eine Möglichkeit zur Verfügung, lang laufende Operationen im Hintergrund auszuführen, ohne dass dabei das UI blockiert wird und einfriert. Ein responsive UI kann somit elegant erstellt werden. Diese Möglichkeiten möchte ich in diesem Artikel kurz vorstellen.

Problemstellung

Das UI, also der Java FX Scene Graph, wird durch den Application Thread gesteuert. Werden in diesem Thread lang laufende Operationen ausgeführt, reagiert das UI nicht mehr und friert ein, bis die Operation erledigt ist. Bei diesen Operationen kann es sich um den Zugriff auf externen Resourcen wie Web Services, lokale Resourcen wie Datenbank oder Filesystem, oder einfach lange Rechenoperationen handeln. Nun könnte man sagen, macht doch nichts, wenn der User eh nicht weiter arbeiten kann, bis das Ergebnis der Operation vorliegt.
Ein gutes UI sollte immer responsiv sein, also immer auf Usereingaben reagieren um dem User nicht den Eindruck zu vermitteln, dass das Programm abgestürzt ist. Spätestens die Windowsmeldung „Anwendung liefert keine Rückmeldung“ wird den User irritieren. Und diese Meldung kommt, wenn der Java FX Application Thread länger blockiert ist.

Es ist also Best Practice lang laufende Operationen in einem eigenen Thread im Hintergrund laufen zu lassen und den Application Thread frei zu halten. Muss der User tatsächlich warten, bis die Operation beendet ist, kann dies über einen Modalen Dialog und einem Progress Bar erfolgen.
Problem beim Multi-Threading in Java FX ist, dass die GUI nicht thread-safe ist und nur über den Application Thread auf diese zugegriffen und sie modifiziert werden darf. Es ist aber eine Kommunikation zwischen dem Hintergrund Thread und dem Application Thread notwendig um das Ergebnis oder den Fortschritt zurück zu melden.

Task und Service

Übersicht

Java FX bietet mit dem javafx.concurrent package eine API an, welche die Entwicklung vom Multi-Threaded Code vereinfacht und resultierende Problemstellungen, insbesondere bei der Thread übergreifenden Kommunikation, berücksichtigt.
Die wichtigsten Basisklassen sind Task und Service, beide implementieren das Worker Interface. Mit diesen Klassen können asynchrone Tasks im Hintergrund ausgeführt und beobachtet werden.

Mit der Klasse Task können Worker erzeugt werden, die im Hintergrund laufen. Ein erzeugter Task kann nur einmal ausgeführt werden. Soll er ein zweites mal ausgeführt werden, muss er neu angelegt werden (siehe Beispiel unten).
Die Klasse Service ist im Prinzip ein Wrapper um einen Task und bietet Methoden zur Verwaltung an (starten, stoppen). Ein angelegter Service kann gestartet, gestoppt und wieder neu gestartet werden, ohne, dass der Service neu angelegt werden muss.
Task eignet sich also für einmalige Operationen, Service für Operationen, die öfters durchgeführt werden, wie z.B. der Zugriff auf Back End Services.

Task

Der Task ist der Worker, der im Hintergrund ausgeführt wird. Für jede Ausführung muss er neu angelegt werden. Die direkte Verwendung von Task eignet sich somit für einmalige Aktionen.
Folgender Code zeigt, wie ein Task angelegt und ausgeführt wird.

Task<Void> task = new Task<Void>() {
   @Override protected Void call() throws Exception {
      // Do some stuff....
      return null;
   }
};

Thread th = new Thread(task);
th.setDaemon(true);
th.start();

In den Zeilen 1-6 wird der Task angelegt. Der Task im Beispiel liefert keinen return Wert zurück. In den Zeilen 8-10 wird der Task mit einem neuen Thread ausgeführt.
Würde man erneut einen Thread mit dem Task anlegen, oder den Thread neu starten, führt das zu einem Fehler. Der instanziierte Task ist also nur einmal ausführbar. Um den Task erneut auszuführen, muss der komplette Code erneut ausgeführt werden.

Weitere Beispiele befinden sich auf der Doku-Seite von Task. Dort befinden sich auch Beispiele von Tasks, die einen return-Wert haben und Beispiele die während der Ausführung den Fortschritt melden.

Service

Die Klasse Service legt einen Task an und führt ihn aus. Der Vorteil ist, dass ein instanziierter Service mehrmals gestartet und getoppt werden kann. Die Verwendung eignet sich also für Aktionen, die öfters ausgeführt werden.
Folgender Code zeigt die Implementierung einer Service-Klasse.

public class MyFirstService extends Service<Void> {

   @Override
   protected Task<Void> createTask() {
      return new Task<Void>() {
         @Override
         protected Void call() throws Exception {
            // Do some stuff
            return null;
         }
      };
   }

}

Die eigene Service-Klasse muss von der Basisklasse Service ableiten. In der Klasse wird der Task mit dem Code implementiert, der im Hintergrund ausgeführt werden soll. Der Task im Beispiel liefert keinen return-Wert zurück.

Folgender Code zeigt, wie der Service angelegt und ausgeführt wird.

MyFirstService s = new MyFirstService();
s.start();

Die Instanz des Service kann mit start() immer wieder neu gestartet werden. Mit cancel() kann ein Abbruch erzwungen werden.

Weitere Beispiele befinden sich auf der Doku-Seite von Service. Dort befindet sich auch ein Beispiel, das einen return-Wert hat.

Interaktion UI und Task

Reaktion auf Task Ereignisse

Ein Task, und damit auch ein Service, kann verschiedene Zustände haben, auf die man reagieren kann. Dabei handelt es sich um CANCELLED, FAILED, RUNNING, SCHEDULED und SUCCEEDED. Auf jeden dieser Zustände kann man einen EventHandler registrieren, der aufgerufen wird, sobald der Zustand erreicht wird.
Das folgende Beispiel zeigt, wie man im UI auf Exceptions reagieren kann, die in einem Task auftreten.

MyFirstService s = new MyFirstService();

s.setOnFailed((event) -> {
   Throwable t = s.getException();
   // Do something with Exception in UI
   s.reset();
   s.start();
});

s.start();

In Zeile 1 wird unser Service instanziiert.
In Zeile 3 wird ein EventHandler im Service registriert, der aufgerufen wird, sobald der Task eine Exception wirft. Im Beispiel wird eine Java 8 Lambda Expression verwendet. Die Exception kann man vom Service abfragen (Zeile 4) und dann damit etwas machen, z.B. eine Fehlermeldung im UI anzeigen. Im Beispiel starten wir den Service in Zeile 7 wieder (durch die Exception wurde er gestoppt). Ein Service kann im FAILED Zustand nicht gestartet werden, deshalb, muss zunächst ein reset gemacht werden (Zeile 6).
In Zeile 10 wird der Service gestartet.

Aktualisierung UI im laufenden Task

Zu warten, bis ein Task beendet ist (mit Erfolg oder Fehler) um das UI zu aktualisieren reicht nicht immer aus. Es gibt Situationen, in denen man, während der Ausführung des Tasks, das UI aktualisieren möchte. Java FX bietet hierfür zwei Methoden an, updateMessage() und updateProgress(). Damit können Messages und der laufende Fortschritt (z.B. für eine Progress Bar) vom Task laufend gemeldet werden. Auf weitere Details zur Nutzung möchte ich hier nicht näher eingehen.

Ich möchte ein Beispiel zeigen, in dem eine Liste oder Tabelle im UI während der Ausführung des Task laufend mit Einträgen erweitert wird.
Zunächst brauchen wir in der Main-Application, eine ObservableList, die über das Java FX Binding mit einer Liste oder Tabelle des UI verbunden ist. Als Hintergrundprozess lassen wir einen Service laufen, der Listen-Einträge ermittelt. Dem Service wird eine Instanz der Main-Application übergeben, um die ObservableList zu aktualisieren.

Folgender Code zeigt den Ausschnitt aus der Main-Application.

public class MyMainApp extends Application {

   // Die ObservableList ,it den Einträgen, die im UI angezeigt werden
   private ObservableList<MyListEntry> listData = FXCollections.observableArrayList();

   /**
    * Returns the observable list
   */
   public ObservableList<MyListEntry> getList() {
        return listData;
   }

   @Override
   public void start(Stage primaryStage) {
      // .... Code for UI start
      MyListService s = new MyListService(this);
      s.start();
   }
}

Folgender Code zeigt den Service.

public class MyListService extends Service<Void> {

   private MyMainApp mainApp;

   public MyListService(MyMainApp mainApp) {
      this.mainApp = mainApp;
   }

   @Override
   protected Task<Void> createTask() {
      return new Task<Void>() {
         @Override
         protected Void call() throws Exception {
            while(true) {
               MyListEntry entry = readAnotherListEntry();
               if (entry == null) {
                  break;
               }
               addEntryInUI(entry);
            }
            return null;
         }
      };
   }

   private void addEntryInUI(MyListEntry entry) {
      Platform.runLater(new Runnable() {
         @Override
         public void run() {
            mainApp.getList().add(entry);
         }
      });
   }

}

Dem Service wird im Konstruktor die Instanz auf die MainApp übergeben (Zeile 5-7).
Im Task läuft eine Schleife, die einen Back-End Service aufruft, der Einträge für die Liste liefert. (Zeile 15). Die Schleife läuft so lange, bis der Back-End Service keine Einträge mehr liefert.
In jedem Schleifen Durchlauf wird ein Listen Eintrag ermittelt und über die Methode addEntryInUI() der ObservableList in der MainApp hinzugefügt (Zeile 30). Problem ist, dass der Hintergrund-Thread, in dem der Task läuft, nicht auf das UI zugreifen darf und somit auch nicht auf die MainApp, die im Application Thread des UI läuft. Um dieses Problem zu lösen, stellt Java FX die Methode Platform.runLater(…) zur Verfügung. Diese stellt eine Aktion (als Runnable) in eine Queue, die vom Applikation Thread abgearbeitet wird (Zeile 27-32).

Durch dieses Verfahren wird die Liste, während der Laufzeit des Service, immer größer. Der User sieht live, wie die Liste immer weitere Einträge bekommt und wartet nicht, vor einem eingefrorenen UI, bis der Service fertig ist. Wir haben ein responsive UI. Man könnte das UI erweitern und dem User z.B. die Möglichkeit geben die Verarbeitung abzubrechen.

Weitere Informationen

Von Oracle gibt es einen guten Artikel, der das Thema detaillierter beleuchtet: Concurrency in JavaFX.

Ein konkretes Beispiel, in dem ich das Beschriebene anwende steht auf GtHub:

One Reply to “Java FX Concurrency – Tasks und Services”

  1. Sehr schön geschrieben und verstädnlich erklärt, nun kann ich meine UI ohne eine böse Execption über den FX-maintrhread aktualisieren super!

Schreibe einen Kommentar

Anmelden um einen Kommentar abzugeben.

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

*