© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
K. Sharan, P. SpäthLearn JavaFX 17https://doi.org/10.1007/978-1-4842-7848-2_24

24. Understanding Concurrency in JavaFX

Kishori Sharan1   and Peter Späth2
(1)
Montgomery, AL, USA
(2)
Leipzig, Sachsen, Germany
 
In this chapter, you will learn:
  • Why you need a concurrency framework in JavaFX

  • How the Worker<V> interface represents a concurrent task

  • How to run a one-time task

  • How to run a reusable task

  • How to run a scheduled task

The examples of this chapter lie in the com.jdojo.concurrent package. In order for them to work, you must add a corresponding line to the module-info.java file:
...
opens com.jdojo.concurrent to javafx.graphics, javafx.base;
...

The Need for a Concurrency Framework

Java (including JavaFX) GUI (graphical user interface) applications are inherently multithreaded. Multiple threads perform different tasks to keep the UI in sync with the user actions. JavaFX, like Swing and AWT, uses a single thread, called the JavaFX Application Thread, to process all UI events. The nodes representing the UI in a scene graph are not thread-safe. Designing nodes that are not thread-safe has advantages and disadvantages. They are faster, as no synchronization is involved. The disadvantage is that they need to be accessed from a single thread to avoid being in an illegal state. JavaFX puts a restriction that a live scene graph must be accessed from one and only one thread, the JavaFX Application Thread. This restriction indirectly imposes another restriction that a UI event should not process a long-running task, as it will make the application unresponsive. The user will get the impression that the application is hung.

The program in Listing 24-1 displays a window as shown in Figure 24-1. It contains three controls:
  • A Label to display the progress of a task

  • A Start button to start the task

  • An Exit button to exit the application

Figure 24-1

An example of an unresponsive UI

// UnresponsiveUI.java
package com.jdojo.concurrent;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class UnresponsiveUI extends Application {
        Label statusLbl = new Label("Not Started...");
        Button startBtn = new Button("Start");
        Button exitBtn = new Button("Exit");
        public static void main(String[] args) {
                Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
                // Add event handlers to the buttons
                startBtn.setOnAction(e -> runTask());
                exitBtn.setOnAction(e -> stage.close());
                HBox buttonBox = new HBox(5, startBtn, exitBtn);
                VBox root = new VBox(10, statusLbl, buttonBox);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("An Unresponsive UI");
                stage.show();
        }
        public void runTask() {
                for(int i = 1; i <= 10; i++) {
                   try {
                     String status = "Processing " + i + " of " + 10;
                     statusLbl.setText(status);
                     System.out.println(status);
                     Thread.sleep(1000);
                   }
                   catch (InterruptedException e) {
                     e.printStackTrace();
                   }
                }
        }
}
Listing 24-1

Performing a Long-Running Task in an Event Handler

The program is very simple. When you click the Start button, a task lasting for ten seconds is started. The logic for the task is in the runTask() method , which simply runs a loop ten times. Inside the loop, the task lets the current thread, which is the JavaFX Application Thread, sleep for one second. The program has two problems.

Click the Start button and immediately try to click the Exit button. Clicking the Exit button has no effect until the task finishes. Once you click the Start button, you cannot do anything else on the window, except to wait for ten seconds for the task to finish. That is, the application becomes unresponsive for ten seconds. This is the reason you named the class UnresponsiveUI .

Inside the loop in the runTask() method , the program prints the status of the task on the standard output and displays the same in the Label in the window. You see the status updated on the standard output, but not in the Label.

It is repeated to emphasize that all UI event handlers in JavaFX run on a single thread, which is the JavaFX Application Thread. When the Start button is clicked, the runTask() method is executed in the JavaFX Application Thread. When the Exit button is clicked while the task is running, an ActionEvent event for the Exit button is generated and queued on the JavaFX Application Thread. The ActionEvent handler for the Exit button is run on the same thread after the thread is done running the runTask() method as part of the ActionEvent handler for the Start button.

A pulse event is generated when the scene graph is updated. The pulse event handler is also run on the JavaFX Application Thread. Inside the loop, the text property of the Label was updated ten times, which generated the pulse events. However, the scene graph was not refreshed to show the latest text for the Label, as the JavaFX Application Thread was busy running the task and it did not run the pulse event handlers.

Both problems arise because there is only one thread to process all UI event handlers, and you ran a long-running task in the ActionEvent handler for the Start button.

What is the solution? You have only one option. You cannot change the single-threaded model for handling the UI events. You must not run long-running tasks in the event handlers. Sometimes, it is a business need to process a big job as part of a user action. The solution is to run the long-running tasks in one or more background threads, instead of in the JavaFX Application Thread.

The program in Listing 24-2 is your first, incorrect attempt to provide a solution. The ActionEvent handler for the Start button calls the startTask() method, which creates a new thread and runs the runTask() method in the new thread.
// BadUI.java
package com.jdojo.concurrent;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class BadUI extends Application {
        Label statusLbl = new Label("Not Started...");
        Button startBtn = new Button("Start");
        Button exitBtn = new Button("Exit");
        public static void main(String[] args) {
                Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
                // Add event handlers to the buttons
                startBtn.setOnAction(e -> startTask());
                exitBtn.setOnAction(e -> stage.close());
                HBox buttonBox = new HBox(5, startBtn, exitBtn);
                VBox root = new VBox(10, statusLbl, buttonBox);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("A Bad UI");
                stage.show();
        }
        public void startTask() {
                // Create a Runnable
                Runnable task = () -> runTask();
                // Run the task in a background thread
                Thread backgroundThread = new Thread(task);
                // Terminate the running thread if the application exits
                backgroundThread.setDaemon(true);
                // Start the thread
                backgroundThread.start();
        }
        public void runTask() {
            for(int i = 1; i <= 10; i++) {
              try {
                String status = "Processing " + i + " of " + 10;
                statusLbl.setText(status);
                System.out.println(status);
                Thread.sleep(1000);
              }
              catch (InterruptedException e) {
                e.printStackTrace();
              }
            }
        }
}
Listing 24-2

A Program Accessing a Live Scene Graph from a Non-JavaFX Application Thread

Run the program and click the Start button. A runtime exception is thrown. The partial stack trace of the exception is as follows:
Exception in thread "Thread-4" java.lang.IllegalStateException:
Not on FX application thread; currentThread = Thread-4
  at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:209)
  at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(
      QuantumToolkit.java:393)...
   at com.jdojo.concurrent.BadUI.runTask(BadUI.java:47)...
The following statement in the runTask() method generated the exception:
statusLbl.setText(status);

The JavaFX runtime checks that a live scene must be accessed from the JavaFX Application Thread. The runTask() method is run on a new thread, named Thread-4 as shown in the stack trace, which is not the JavaFX Application Thread. The foregoing statement sets the text property for the Label, which is part of a live scene graph, from the thread other than the JavaFX Application Thread, which is not permissible.

How do you access a live scene graph from a thread other than the JavaFX Application Thread? The simple answer is that you cannot. The complex answer is that when a thread wants to access a live scene graph, it needs to run the part of the code that accesses the scene graph in the JavaFX Application Thread. The Platform class in the javafx.application package provides two static methods to work with the JavaFX Application Thread:
  • public static boolean isFxApplicationThread()

  • public static void runLater(Runnable runnable)

The isFxApplicationThread() method returns true if the thread calling this method is the JavaFX Application Thread. Otherwise, it returns false.

The runLater() method schedules the specified Runnable to be run on the JavaFX Application Thread at some unspecified time in future.

Tip

If you have experience working with Swing, the Platform.runLater() in JavaFX is the counterpart of the SwingUtilities.invokeLater() in Swing.

Let us fix the problem in the BadUI application. The program in Listing 24-3 is the correct implementation of the logic to access the live scene graph. Figure 24-2 shows a snapshot of the window displayed by the program.
// ResponsiveUI.java
package com.jdojo.concurrent;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ResponsiveUI extends Application {
        Label statusLbl = new Label("Not Started...");
        Button startBtn = new Button("Start");
        Button exitBtn = new Button("Exit");
        public static void main(String[] args) {
                Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
                // Add event handlers to the buttons
                startBtn.setOnAction(e -> startTask());
                exitBtn.setOnAction(e -> stage.close());
                HBox buttonBox = new HBox(5, startBtn, exitBtn);
                VBox root = new VBox(10, statusLbl, buttonBox);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("A Responsive UI");
                stage.show();
        }
        public void startTask() {
                // Create a Runnable
                Runnable task = () -> runTask();
                // Run the task in a background thread
                Thread backgroundThread = new Thread(task);
                // Terminate the running thread if the application exits
                backgroundThread.setDaemon(true);
                // Start the thread
                backgroundThread.start();
        }
        public void runTask() {
          for(int i = 1; i <= 10; i++) {
            try {
              String status = "Processing " + i + " of " + 10;
              // Update the Label on the JavaFx Application Thread
              Platform.runLater(() -> statusLbl.setText(status));
              System.out.println(status);
              Thread.sleep(1000);
            }
            catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
        }
}
Listing 24-3

A Responsive UI That Runs Long-Running Tasks in a Background Thread

Figure 24-2

A UI that runs a task in a background thread and updates the live scene graph correctly

The program replaces the statement
statusLbl.setText(status);
in the BadUI class with the statement
// Update the Label on the JavaFx Application Thread
Platform.runLater(() -> statusLbl.setText(status));

Now, setting the text property for the Label takes place on the JavaFX Application Thread. The ActionEvent handler of the Start button runs the task in a background thread, thus freeing up the JavaFX Application Thread to handle user actions. The status of the task is updated in the Label regularly. You can click the Exit button while the task is being processed.

Did you overcome the restrictions imposed by the event-dispatching threading model of the JavaFX? The answer is yes and no. You used a trivial example to demonstrate the problem. You have solved the trivial problem. However, in the real world, performing a long-running task in a GUI application is not so trivial. For example, your task-running logic and the UI are tightly coupled as you are referencing the Label inside the runTask() method, which is not desirable in the real world. Your task does not return a result, nor does it have a reliable mechanism to handle errors that may occur. Your task cannot be reliably cancelled, restarted, or scheduled to be run at a future time.

The JavaFX concurrency framework has answers to all these questions. The framework provides a reliable way of running a task in one or multiple background threads and publishing the status and the result of the task in a GUI application. The framework is the topic of discussion in this chapter. I have taken several pages just to make the case for a concurrency framework in JavaFX. If you understand the background of the problem as presented in this section, understanding the framework will be easy.

Understanding the Concurrency Framework API

Java contains a comprehensive concurrency framework to the Java programming language through the libraries in the java.util.concurrent package. The JavaFX concurrency framework is very small. It is built on top of the Java language concurrency framework keeping in mind that it will be used in a GUI environment. Figure 24-3 shows a class diagram of the classes in the JavaFX concurrency framework.
Figure 24-3

A class diagram for classes in the JavaFX concurrency framework

The framework consists of one interface, four classes, and one enum.

An instance of the Worker interface represents a task that needs to be performed in one or more background threads. The state of the task is observable from the JavaFX Application Thread.

The Task, Service, and ScheduledService classes implement the Worker interface. They represent different types of tasks. They are abstract classes. An instance of the Task class represents a one-shot task. A Task cannot be reused. An instance of the Service class represents a reusable task. The ScheduledService class inherits from the Service class. A ScheduledService is a task that can be scheduled to run repeatedly after a specified interval.

The constants in the Worker.State enum represent different states of a Worker.

An instance of the WorkerStateEvent class represents an event that occurs as the state of a Worker changes. You can add event handlers to all three types of tasks to listen to the change in their states.

Understanding the Worker<V> Interface

The Worker<V> interface provides the specification for any task performed by the JavaFX concurrency framework. A Worker is a task that is performed in one or more background threads. The generic parameter V is the data type of the result of the Worker. Use Void as the generic parameter if the Worker does not produce a result. The state of the task is observable. The state of the task is published on the JavaFX Application Thread, making it possible for the task to communicate with the scene graph, as is commonly required in a GUI application.

State Transitions for a Worker

During the life cycle, a Worker transitions through different states. The constants in the Worker.State enum represent the valid states of a Worker:
  • Worker.State.READY

  • Worker.State.SCHEDULED

  • Worker.State.RUNNING

  • Worker.State.SUCCEEDED

  • Worker.State.CANCELLED

  • Worker.State.FAILED

Figure 24-4 shows the possible state transitions of a Worker with the Worker.State enum constants representing the states.
Figure 24-4

Possible state transition paths for a Worker

When a Worker is created, it is in the READY state. It transitions to the SCHEDULED state, before it starts executing. When it starts running, it is in the RUNNING state. Upon successful completion, a Worker transitions from the RUNNING state to the SUCCEEDED state. If the Worker throws an exception during its execution, it transitions to the FAILED state. A Worker may be cancelled using the cancel() method. It may transition to the CANCELLED state from the READY, SCHEDULED, and RUNNING states. These are the normal state transitions for a one-shot Worker.

A reusable Worker may transition from the CANCELLED, SUCCEEDED, and FAILED states to the READY state as shown in the figure by dashed lines.

Properties of a Worker

The Worker interface contains nine read-only properties that represent the internal state of the task:
  • title

  • message

  • running

  • state

  • progress

  • workDone

  • totalWork

  • value

  • exception

When you create a Worker, you will have a chance to specify these properties. The properties can also be updated as the task progresses.

The title property represents the title for the task. Suppose a task generates prime numbers. You may give the task a title “Prime Number Generator.”

The message property represents a detailed message during the task processing. Suppose a task generates several prime numbers; you may want to give feedback to the user at a regular interval or at appropriate times with a message such as “Generating X of Y prime numbers.”

The running property tells whether the Worker is running. It is true when the Worker is in the SCHEDULED or RUNNING states. Otherwise, it is false.

The state property specifies the state of the Worker. Its value is one of the constants of the Worker.State enum.

The totalWork, workDone, and progress properties represent the progress of the task. The totalWork is the total amount of work to be done. The workDone is the amount of work that has been done. The progress is the ratio of workDone and totalWork. They are set to –1.0 if their values are not known.

The value property represents the result of the task. Its value is non-null only when the Worker finishes successfully reaching the SUCCEEDED state. Sometimes, a task may not produce a result. In those cases, the generic parameter V would be Void, and the value property will always be null.

A task may fail by throwing an exception. The exception property represents the exception that is thrown during the processing of the task. It is non-null only when the state of the Worker is FAILED. It is of the type Throwable.

Typically, when a task is in progress, you want to display the task details in a scene graph. The concurrency framework makes sure that the properties of a Worker are updated on the JavaFX Application Thread. Therefore, it is fine to bind the properties of the UI elements in a scene graph to these properties. You can also add Invalidation and ChangeListener to these properties and access a live scene graph from inside those listeners.

In subsequent sections, you will discuss specific implementations of the Worker interface. Let us create a reusable GUI to use in all examples. The GUI is based on a Worker to display the current values of its properties.

Utility Classes for Examples

Let us create the reusable GUI and non-GUI parts of the programs to use in examples in the subsequent sections. The WorkerStateUI class in Listing 24-4 builds a GridPane to display all properties of a Worker. It is used with a Worker<ObservableList<Long>>. It displays the properties of a Worker by UI elements to them. You can bind properties of a Worker to the UI elements by passing a Worker to the constructor or calling the bindToWorker() method.
// WorkerStateUI.java
package com.jdojo.concurrent;
import javafx.beans.binding.When;
import javafx.collections.ObservableList;
import javafx.concurrent.Worker;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TextArea;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
public class WorkerStateUI extends GridPane {
        private final Label title = new Label("");
        private final Label message = new Label("");
        private final Label running = new Label("");
        private final Label state = new Label("");
        private final Label totalWork = new Label("");
        private final Label workDone = new Label("");
        private final Label progress = new Label("");
        private final TextArea value = new TextArea("");
        private final TextArea exception = new TextArea("");
        private final ProgressBar progressBar = new ProgressBar();
        public WorkerStateUI() {
                addUI();
        }
        public WorkerStateUI(Worker<ObservableList<Long>> worker) {
                addUI();
                bindToWorker(worker);
        }
        private void addUI() {
                value.setPrefColumnCount(20);
                value.setPrefRowCount(3);
                exception.setPrefColumnCount(20);
                exception.setPrefRowCount(3);
                this.setHgap(5);
                this.setVgap(5);
                addRow(0, new Label("Title:"), title);
                addRow(1, new Label("Message:"), message);
                addRow(2, new Label("Running:"), running);
                addRow(3, new Label("State:"), state);
                addRow(4, new Label("Total Work:"), totalWork);
                addRow(5, new Label("Work Done:"), workDone);
                addRow(6, new Label("Progress:"),
                         new HBox(2, progressBar, progress));
                addRow(7, new Label("Value:"), value);
                addRow(8, new Label("Exception:"), exception);
        }
        public void bindToWorker(final Worker<ObservableList<Long>> worker) {
                // Bind Labels to the properties of the worker
                title.textProperty().bind(worker.titleProperty());
                message.textProperty().bind(worker.messageProperty());
                running.textProperty().bind(
                         worker.runningProperty().asString());
                state.textProperty().bind(
                         worker.stateProperty().asString());
                totalWork.textProperty().bind(
                         new When(worker.totalWorkProperty().isEqualTo(-1))
                    .then("Unknown")
                    .otherwise(worker.totalWorkProperty().asString()));
                workDone.textProperty().bind(
                         new When(worker.workDoneProperty().isEqualTo(-1))
                    .then("Unknown")
                    .otherwise(worker.workDoneProperty().asString()));
                progress.textProperty().bind(
                         new When(worker.progressProperty().isEqualTo(-1))
                    .then("Unknown")
                    .otherwise(worker.progressProperty().multiply(100.0)
                            .asString("%.2f%%")));
                progressBar.progressProperty().bind(
                         worker.progressProperty());
                value.textProperty().bind(
                         worker.valueProperty().asString());
                // Display the exception message when an exception occurs
                     // in the worker
                worker.exceptionProperty().addListener(
                         (prop, oldValue, newValue) -> {
                        if (newValue != null) {
                            exception.setText(newValue.getMessage());
                        } else {
                            exception.setText("");
                        }
                });
        }
}
Listing 24-4

A Utility Class to Build UI Displaying the Properties of a Worker

The PrimeUtil class in Listing 24-5 is a utility class to check whether a number is a prime number.
// PrimeUtil.java
package com.jdojo.concurrent;
public class PrimeUtil {
        public static boolean isPrime(long num) {
            if (num <= 1 || num % 2 == 0) {
                    return false;
            }
            int upperDivisor = (int)Math.ceil(Math.sqrt(num));
            for (int divisor = 3; divisor <= upperDivisor; divisor += 2) {
                    if (num % divisor == 0) {
                            return false;
                    }
            }
            return true;
        }
}
Listing 24-5

A Utility Class to Work with Prime Numbers

Using the Task<V> Class

An instance of the Task<V> class represents a one-time task. Once the task is completed, cancelled, or failed, it cannot be restarted. The Task<V> class implements the Worker<V> interface. Therefore, all properties and methods specified by the Worker<V> interface are available in the Task<V> class.

The Task<V> class inherits from the FutureTask<V> class , which is part of the Java concurrency framework. The FutureTask<V> implements the Future<V>, RunnableFuture<V>, and Runnable interfaces. Therefore, a Task<V> also implements all these interfaces.

Creating a Task

How do you create a Task<V>? Creating a Task<V> is easy. You need to subclass the Task<V> class and provide an implementation for the abstract method call(). The call() method contains the logic to perform the task. The following snippet of code shows the skeleton of a Task implementation:
// A Task that produces an ObservableList<Long>
public class PrimeFinderTask extends Task<ObservableList<Long>> {
        @Override
        protected ObservableList<Long>> call() {
                // Implement the task logic here...
        }
}

Updating Task Properties

Typically, you would want to update the properties of the task as it progresses. The properties must be updated and read on the JavaFX Application Thread, so they can be observed safely in a GUI environment. The Task<V> class provides special methods to update some of its properties:
  • protected void updateMessage(String message)

  • protected void updateProgress(double workDone, double totalWork)

  • protected void updateProgress(long workDone, long totalWork)

  • protected void updateTitle(String title)

  • protected void updateValue(V value)

You provide the values for the workDone and the totalWork properties to the updateProgress() method. The progress property will be set to workDone/totalWork. The method throws a runtime exception if the workDone is greater than the totalWork or both are less than –1.0.

Sometimes, you may want to publish partial results of a task in its value property. The updateValue() method is used for this purpose. The final result of a task is the return value of its call() method.

All updateXxx() methods are executed on the JavaFX Application Thread. Their names indicate the property they update. They are safe to be called from the call() method of the Task. If you want to update the properties of the Task from the call() method directly, you need to wrap the code inside a Platform.runLater() call.

Listening to Task Transition Events

The Task class contains the following properties to let you set event handlers for its state transitions:
  • onCancelled

  • onFailed

  • onRunning

  • onScheduled

  • onSucceeded

The following snippet of code adds an onSucceeded event handler, which would be called when the task transitions to the SUCCEEDED state:
Task<ObservableList<Long>> task = create a task...
task.setOnSucceeded(e -> {
        System.out.println("The task finished. Let us party!")
});

Cancelling a Task

Use one of the following two cancel() methods to cancel a task:
  • public final boolean cancel()

  • public boolean cancel(boolean mayInterruptIfRunning)

The first version removes the task from the execution queue or stops its execution. The second version lets you specify whether the thread running the task be interrupted. Make sure to handle the InterruptedException inside the call() method. Once you detect this exception, you need to finish the call() method quickly. Otherwise, the call to cancel(true) may not cancel the task reliably. The cancel() method may be called from any thread.

The following methods of the Task are called when it reaches a specific state:
  • protected void scheduled()

  • protected void running()

  • protected void succeeded()

  • protected void cancelled()

  • protected void failed()

Their implementations in the Task class are empty. They are meant to be overridden by the subclasses.

Running a Task

A Task is Runnable as well as a FutureTask . To run it, you can use a background thread or an ExecutorService:
// Schedule the task on a background thread
Thread backgroundThread = new Thread(task);
backgroundThread.setDaemon(true);
backgroundThread.start();
// Use the executor service to schedule the task
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(task);

A Prime Finder Task Example

It is time to see a Task in action. The program in Listing 24-6 is an implementation of the Task<ObservableList<Long>>. It checks for prime numbers between the specified lowerLimit and upperLimit. It returns all the numbers in the range. Notice that the task thread sleeps for a short time before checking a number for a prime number. This is done to give the user an impression of a long-running task. It is not needed in a real-world application. The call() method handles an InterruptedException and finishes the task if the task was interrupted as part of a cancellation request.

The call to the method updateValue() needs little explanation:
updateValue(FXCollections.<Long>unmodifiableObservableList(results));

Every time a prime number is found, the results list is updated. The foregoing statement wraps the results list in an unmodifiable observable list and publishes it for the client. This gives the client access to the partial results of the task. This is a quick and dirty way of publishing the partial results. If the call() method returns a primitive value, it is fine to call the updateValue() method repeatedly.

Tip

In this case, you are creating a new unmodifiable list every time you find a new prime number, which is not acceptable in a production environment for performance reasons. The efficient way of publishing the partial results would be to declare a read-only property for the Task; update the read-only property regularly on the JavaFX Application Thread; let the client bind to the read-only property to see the partial results.

// PrimeFinderTask.java
// ...find in the book's download area.
Listing 24-6

Finding Prime Numbers Using a Task<Long>

The program in Listing 24-7 contains the complete code to build a GUI using your PrimeFinderTask class. Figure 24-5 shows the window when the task is running. You will need to click the Start button to start the task. Clicking the Cancel button cancels the task. Once the task finishes, it is cancelled or it fails; you cannot restart it, and both the Start and Cancel buttons are disabled. Notice that when the task finds a new prime number, it is displayed on the window immediately.
// OneShotTask.java
// ...find in the book's download area.
Listing 24-7

Executing a Task in a GUI Environment

Figure 24-5

A window using the prime number finder Task

Using the Service<V> Class

The Service<V> class is an implementation of the Worker<V> interface. It encapsulates a Task<V>. It makes the Task<V> reusable by letting it be started, cancelled, reset, and restarted.

Creating the Service

Remember that a Service<V> encapsulates a Task<V>. Therefore, you need a Task<V> to have a Service<V>. The Service<V> class contains an abstract protected createTask() method that returns a Task<V>. To create a service, you need to subclass the Service<V> class and provide an implementation for the createTask() method.

The following snippet of code creates a Service that encapsulates a PrimeFinderTask, which you have created earlier:
// Create a service
Service<ObservableList<Long>> service = new Service<ObservableList<Long>>() {
        @Override
        protected Task<ObservableList<Long>> createTask() {
                // Create and return a Task
                return new PrimeFinderTask();
        }
};

The createTask() method of the service is called whenever the service is started or restarted.

Updating Service Properties

The Service class contains all properties (title, message, state, value, etc.) that represent the internal state of a Worker. It adds an executor property, which is a java.util.concurrent.Executor. The property is used to run the Service. If it is not specified, a daemon thread is created to run the Service.

Unlike the Task class, the Service class does not contain updateXxx() methods for updating its properties. Its properties are bound to the corresponding properties of the underlying Task<V>. When the Task updates its properties, the changes are reflected automatically to the Service and to the client.

Listening to Service Transition Events

The Service class contains all properties for setting the state transition listeners as contained by the Task class. It adds an onReady property. The property specifies a state transition event handler, which is called when the Service transitions to the READY state. Note that the Task class does not contain an onReady property as a Task is in the READY state when it is created, and it never transitions to the READY state again. However, a Service can be in the READY state multiple times. A Service transitions to the READY state when it is created, reset, and restarted. The Service class also contains a protected ready() method, which is intended to be overridden by subclasses. The ready() method is called when the Service transitions to the READY state.

Cancelling the Service

Use the cancel() methods to cancel a Service: the method sets the state of the Service to CANCELLED.

Starting the Service

Calling the start() method of the Service class starts a Service. The method calls the createTask() method to get a Task instance and runs the Task. The Service must be in the READY state when its start() method is called:
Service<ObservableList<Long>> service = create a service
...
// Start the service
service.start();

Resetting the Service

Calling the reset() method of the Service class resets the Service. Resetting puts all the Service properties back to their initial states. The state is set to READY. Resetting a Service is allowed only when the Service is in one of the finish states: SUCCEEDED, FAILED, CANCELLED, or READY. Calling the reset() method throws a runtime exception if the Service is in the SCHEDULED or RUNNING state.

Restarting the Service

Calling the restart() method of the Service class restarts a Service. It cancels the task if it exists, resets the service, and starts it. It calls the three methods on the Service object in sequence:
  • cancel()

  • reset()

  • start()

The Prime Finder Service Example

The program in Listing 24-8 shows how to use a Service. The Service object is created and stored as an instance variable. The Service object manages a PrimeFinderTask object , which is a Task to find prime numbers between two numbers. Four buttons are added: Start/Restart, Cancel, Reset, and Exit. The Start button is labeled Restart after the Service is started for the first time. The buttons do what their labels indicate. Buttons are disabled when you cannot invoke them. Figure 24-6 shows a screenshot of the window after the Start button is clicked.
// PrimeFinderService.java
// ...find in the book's download area.
Listing 24-8

Using a Service to Find Prime Numbers

Figure 24-6

A window using a Service to find prime numbers

Using the ScheduledService<V> Class

The ScheduledService<V> is a Service<V>, which automatically restarts. It can restart when it finishes successfully or when it fails. Restarting on a failure is configurable. The ScheduledService<V> class inherits from the Service<V> class. The ScheduledService is suitable for tasks that use polling. For example, you may use it to refresh the score of a game or the weather report from the Internet after every ten minutes.

Creating the ScheduledService

The process of creating a ScheduledService is the same as that of creating a Service. You need to subclass the ScheduledService<V> class and provide an implementation for the createTask() method.

The following snippet of code creates a ScheduledService that encapsulates a PrimeFinderTask, which you have created earlier:
// Create a scheduled service
ScheduledService<ObservableList<Long>> service =
    new ScheduledService <ObservableList<Long>>() {
        @Override
        protected Task<ObservableList<Long>> createTask() {
                // Create and return a Task
                return new PrimeFinderTask();
        }
};

The createTask() method of the service is called when the service is started or restarted manually or automatically. Note that a ScheduledService is automatically restarted. You can start and restart it manually by calling the start() and restart() methods.

Tip

Starting, cancelling, resetting, and restarting a ScheduledService work the same way as these operations on a Service.

Updating ScheduledService Properties

The ScheduledService<ScheduledService> class inherits properties from the Service<V> class. It adds the following properties that can be used to configure the scheduling of the service:
  • lastValue

  • delay

  • period

  • restartOnFailure

  • maximumFailureCount

  • backoffStrategy

  • cumulativePeriod

  • currentFailureCount

  • maximumCumulativePeriod

A ScheduledService<V> is designed to run several times. The current value computed by the service is not very meaningful. Your class adds a new property lastValue, which is of the type V, and it is the last value computed by the service.

The delay is a Duration, which specifies a delay between when the service is started and when it begins running. The service stays in the SCHEDULED state for the specified delay. The delay is honored only when the service is started manually calling the start() or restart() method. When the service is restarted automatically, honoring the delay property depends on the current state of the service. For example, if the service is running behind its periodic schedule, it will rerun immediately, ignoring the delay property. The default delay is zero.

The period is a Duration, which specifies the minimum amount of time between the last run and the next run. The default period is zero.

The restartOnFailure specifies whether the service restarts automatically when it fails. By default, it is set to true.

The currentFailureCount is the number of times the scheduled service has failed. It is reset to zero when the scheduled service is restarted manually.

The maximumFailureCount specifies the maximum number of times the service can fail before it is transitioned into the FAILED state, and it is not automatically restarted again. Note that you can restart a scheduled service any time manually. By default, it is set to Integer.MAX_VALUE.

The backoffStrategy is a Callback<ScheduledService<?>,Duration> that computes the Duration to add to the period on each failure. Typically, if a service fails, you want to slow down before retrying it. Suppose a service runs every 10 minutes. If it fails for the first time, you may want to restart it after 15 minutes. If it fails for the second time, you want to increase the rerun time to 25 minutes, and so on. The ScheduledService class provides three built-in backoff strategies as constants:
  • EXPONENTIAL_BACKOFF_STRATEGY

  • LINEAR_BACKOFF_STRATEGY

  • LOGARITHMIC_BACKOFF_STRATEGY

The rerun gaps are computed based on the nonzero period and the current failure count. The time between consecutive failed runs increases exponentially in the exponential backoffStrategy, linearly in the linear backoffStrategy, and logarithmically in the logarithmic backoffStrategy. The LOGARITHMIC_BACKOFF_STRATEGY is the default. When the period is zero, the following formulas are used. The computed duration is in milliseconds:
  • Exponential: Math.exp(currentFailureCount)

  • Linear: currentFailureCount

  • Logarithmic: Math.log1p(currentFailureCount)

The following formulas are used for the non-null period:
  • Exponential: period + (period * Math.exp(currentFailureCount)

  • Linear: period + (period * currentFailureCount)

  • Logarithmic: period + (period * Math.log1p(currentFailureCount))

The cumulativePeriod is a Duration, which is the time between the current failed run and the next run. Its value is computed using the backoffStrategy property. It is reset upon a successful run of the scheduled service. Its value can be capped using the maximumCumulativePeriod property.

Listening to ScheduledService Transition Events

The ScheduledService goes through the same transition states as the Service. It goes through the READY, SCHEDULED, and RUNNING states automatically after a successful run. Depending on how the scheduled service is configured, it may go through the same state transitions automatically after a failed run.

You can listen to the state transitions and override the transition-related methods (ready(), running(), failed(), etc.) as you can for a Service. When you override the transition-related methods in a ScheduledService subclass, make sure to call the super method to keep your ScheduledService working properly.

The Prime Finder ScheduledService Example

Let us use the PrimeFinderTask with a ScheduledService. Once started, the ScheduledService will keep rerunning forever. If it fails five times, it will quit by transitioning itself to the FAILED state. You can cancel and restart the service manually any time.

The program in Listing 24-9 shows how to use a ScheduledService. The program is very similar to the one shown in Listing 24-8, except at two places. The service is created by subclassing the ScheduledService class:
// Create the scheduled service
ScheduledService<ObservableList<Long>> service = new ScheduledService<ObservableList<Long>>() {
        @Override
        protected Task<ObservableList<Long>> createTask() {
                return new PrimeFinderTask();
        }
};
The ScheduledService is configured in the beginning of the start() method, setting the delay, period, and maximumFailureCount properties:
// Configure the scheduled service
service.setDelay(Duration.seconds(5));
service.setPeriod(Duration.seconds(30));
service.setMaximumFailureCount(5);
Figures 24-7, 24-8, and 24-9 show the state of the ScheduledService when it is not started, when it is observing the delay period in the SCHEDULED state, and when it is running. Use the Cancel and Reset buttons to cancel and reset the service. Once the service is cancelled, you can restart it manually by clicking the Restart button.
// PrimeFinderScheduledService.java
// ...find in the book's download area.
Listing 24-9

Using a ScheduledService to Run a Task

Figure 24-7

The ScheduledService is not started

Figure 24-8

The ScheduledService is started for the first time, and it is observing the delay period

Figure 24-9

The ScheduledService is started and running

Summary

Java (including JavaFX) GUI applications are inherently multithreaded. Multiple threads perform different tasks to keep the UI in sync with the user actions. JavaFX, like Swing and AWT, uses a single thread, called the JavaFX Application Thread, to process all UI events. The nodes representing the UI in a scene graph are not thread-safe. Designing nodes that are not thread-safe has advantages and disadvantages. They are faster, as no synchronization is involved. The disadvantage is that they need to be accessed from a single thread to avoid being in an illegal state. JavaFX puts a restriction that a live scene graph must be accessed from one and only one thread, the JavaFX Application Thread. This restriction indirectly imposes another restriction that a UI event should not process a long-running task, as it will make the application unresponsive. The user will get the impression that the application is hung. The JavaFX concurrency framework is built on top of the Java language concurrency framework keeping in mind that it will be used in a GUI environment. The framework consists of one interface, four classes, and one enum. It provides a way to design a multithreaded JavaFX application that can perform long-running tasks in worker threads, keeping the UI responsive.

An instance of the Worker interface represents a task that needs to be performed in one or more background threads. The state of the task is observable from the JavaFX Application Thread. The Task, Service, and ScheduledService classes implement the Worker interface. They represent different types of tasks. They are abstract classes.

An instance of the Task class represents a one-shot task. A Task cannot be reused.

An instance of the Service class represents a reusable task.

The ScheduledService class inherits from the Service class. A ScheduledService is a task that can be scheduled to run repeatedly after a specified interval.

The constants in the Worker.State enum represent different states of a Worker. An instance of the WorkerStateEvent class represents an event that occurs as the state of a Worker changes. You can add event handlers to all three types of tasks to listen to the change in their states.

The next chapter will discuss how to incorporate audios and videos in JavaFX applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset