When we develop a graphical application, we usually have to respect a UI thread confinement rule: user-interface objects are expected to be accessed from a specific thread. In JavaFX, this thread is called the JavaFX Application Thread.
The JavaFX scene graph is not generally thread-safe. Once nodes are attached to a live scene, they should be created and modified on the JavaFX Application Thread. Event handlers also run on this thread. As a consequence, we should not perform long-running or blocking work in event handlers, because the JavaFX Application Thread would be busy and the interface would stop responding until that work finishes.
JavaFX animations such as Timeline and the subclasses of Transition do not solve this problem by running the animation code on a background thread. JavaFX animations run on the JavaFX Application Thread. They are appropriate for changing UI properties over time, but they must not contain long-running or blocking work. CPU-intensive tasks, file operations, network operations and other potentially slow work should normally be moved to a background thread.
Let’s see this problem with a JavaFX example:
public class Example_JavaFXThreads extends Application
{
// Copy progress
int progress;
public static void main(String[] args)
{
launch(args);
}
@Override
public void start(Stage primaryStage)
{
Label lblProgress = new Label("");
Button btnStart1 = new Button("Start copy (1)");
btnStart1.setOnAction(e ->
{
for (int progress = 1; progress <= 10; progress++)
{
try
{
Thread.sleep(1000);
lblProgress.setText("" + (progress*10)
+ "% completed");
} catch (Exception ex) {}
}
});
VBox vb = new VBox(20);
vb.setAlignment(Pos.CENTER);
vb.getChildren().addAll(lblProgress, btnStart1);
Scene scene = new Scene(vb, 300, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
}
The application of the example simulates the copy of a large file, and when we press the Start copy (1) button, a message is printed every second showing the percentage of file that has been copied for now. If we try to run the application and we click on the Start copy (1) button, we will find out that:
Why does this happen? As we have said before, all the event handling of the application runs on the main application thread. So, when it is sleeping and changing the label’s text in the event loop, nothing else is running (the whole application is waiting for this event to finish).
We could think that, to solve the problem shown in previous example, we could just call a thread that does the file copy and updates the label progress. Let’s do it. In order to keep the original program in its original version, we are going to add a new button, Start copy (2), and we are going to create a thread in its ActionEvent to do the same task that we did before in the event handler of the first button.
We would add the button with the event handler:
Button btnStart2 = new Button("Start copy (2)");
// "Start copy (2)" event: calling a thread to do the task
btnStart2.setOnAction(e ->
{
Thread t = new Start2Thread(lblProgress);
t.start();
});
And then we would add the thread class. We pass the Label as a parameter to have it accessible. In the run method we copy the same code that we used for the btnStart1 event.
class Start2Thread extends Thread
{
// Progress label to update its text
Label lblProgress;
public Start2Thread(Label lblProgress)
{
this.lblProgress = lblProgress;
}
@Override
public void run()
{
for (int progress = 1; progress <= 10; progress++)
{
try
{
Thread.sleep(1000);
lblProgress.setText("" + (progress*10) + "% completed");
} catch (Exception ex) {}
}
}
}
If we click on this second button, we are breaking the JavaFX threading rules. Updating a node that belongs to a live scene graph from a background thread is unsafe and can result in a runtime exception such as IllegalStateException. The problematic line is this one inside the run method:
lblProgress.setText("" + (progress*10) + "% completed");
As we have said before, once we are working with the live scene graph, UI updates must be performed on the JavaFX Application Thread.
The problem when using a secondary thread is that it cannot directly modify the live JavaFX scene graph. For small updates, JavaFX provides Platform.runLater, which places a Runnable in the JavaFX event queue so that it will be executed later on the JavaFX Application Thread.
Inside our loop we can write:
for (int progress = 1; progress <= 10; progress++)
{
try
{
Thread.sleep(1000);
// Lambda expressions can only capture final or effectively-final
// local variables, so we copy the current value.
final int currentProgress = progress;
Platform.runLater(() ->
lblProgress.setText((currentProgress * 10) + "% completed"));
}
catch (InterruptedException ex)
{
Thread.currentThread().interrupt();
break;
}
}
We have introduced the Platform.runLater method. It posts a task to the JavaFX event queue and returns immediately. The task will be executed on the JavaFX Application Thread at some unspecified time in the future. Tasks passed to runLater are processed in the order in which they are posted.
We can check whether the current code is already running on the JavaFX Application Thread by using:
Platform.isFxApplicationThread()
Platform.runLater is useful for occasional UI updates from background code, but it should not be flooded with thousands of very small updates because this can overload the JavaFX event queue. For more complex background operations, JavaFX provides the concurrency framework explained in the next section.
Exercise 1:
Create a project called My3Counters. It must have 3 buttons and 3 labels:
- A button with the text From 1 to 10 that will start a thread that counts from 1 to 10, showing the current number in the corresponding label, and sleeping 1 second after showing each number.
- A button with the text From 1 to 5, with its corresponding label, to count from 1 to 5 (1 number per second as well)
- A button with the text From 10 to 1, with its corresponding label, to count from 10 to 1 (a number per second too).
As soon as we click on a button, its corresponding counting will start, and the button will be disabled (use the setDisable method from the Button object). We may run the three tasks at the same time if we want to. Here you can see a screenshot of the application.
The previous example combines ordinary Java threads with Platform.runLater. This is valid for simple situations, but JavaFX provides a higher-level concurrency API that makes it easier to manage background work, progress, cancellation, results and state changes.
The main elements are:
Worker<V> interface. It represents a unit of work whose state and observable properties can be monitored from JavaFX. Its possible states are READY, SCHEDULED, RUNNING, SUCCEEDED, CANCELLED and FAILED.Task<V> abstract class. It implements Worker<V> and RunnableFuture<V>. A Task is one-shot: once it has run, it cannot be reused.Service<V> abstract class. It represents reusable background work. Every execution creates a new Task through its createTask() method.ScheduledService<V> class. It extends Service<V> and automatically schedules repeated executions.WorkerStateEvent, together with handlers such as setOnRunning, setOnSucceeded, setOnCancelled and setOnFailed, lets the application react to worker state transitions.The call() method of a Task runs on a background thread. It must not directly manipulate the live scene graph. However, methods such as updateMessage, updateProgress, updateTitle and updateValue are specifically designed to be called from the background task; JavaFX safely publishes those changes to the observable worker properties.
The threading rule does not change if the background task happens to run on a virtual thread: a virtual thread is still not the JavaFX Application Thread and therefore must not directly modify the live scene graph.
We are going to see an example of creating a Service and running it in background. We are going to solve the same problem shown in previous example (the simulation of a file copy) with a service. In this new example, we are going to add the possibility of cancelling the copy while it is running, an essential ability of Service class.
Our service class would look like this one:
class FileService extends Service<String>
{
@Override
protected Task<String> createTask()
{
return new Task<String>()
{
@Override
protected String call() throws Exception
{
for (int progress = 1; progress <= 10; progress++)
{
if (isCancelled())
break;
try
{
Thread.sleep(1000);
}
catch (InterruptedException ex)
{
// Cancellation may interrupt a blocking operation.
if (isCancelled())
break;
Thread.currentThread().interrupt();
throw ex;
}
if (isCancelled())
break;
updateMessage((progress * 10) + "% completed");
updateProgress(progress, 10);
}
return isCancelled() ? "Copy cancelled" : "Copy completed";
}
};
}
}
We extend Service<String> because this service will produce a String result when it succeeds. A Service does not normally contain the background algorithm directly: we override createTask() and return a fresh Task<String> for each execution.
The call() method of that task runs on a background thread. Instead of obtaining the Label and modifying it directly, the task calls updateMessage and updateProgress. These methods are designed for background use and update the corresponding observable JavaFX properties safely.
Cancellation is cooperative. Calling cancel() requests cancellation, but the code inside call() should cooperate by checking isCancelled(). Blocking methods such as Thread.sleep() may also throw InterruptedException when the task is cancelled, so the task must handle that situation rather than silently ignoring the exception.
Our main JavaFX controller would be like this:
public class FXServiceExampleController implements Initializable
{
@FXML
private Button btnStart;
@FXML
private Label lblProgress;
@FXML
private Button btnCancel;
FileService service;
@FXML
private void start(ActionEvent event)
{
setProperties(true, false);
service.start();
}
@FXML
private void cancel(ActionEvent event)
{
setProperties(false, true);
service.cancel();
}
@Override
public void initialize(URL url, ResourceBundle rb)
{
service = new FileService();
// Events to be fired when service finishes/cancels/fails...
service.setOnSucceeded(e -> {
setProperties(false, true);
System.out.println(service.getValue());
service.reset();
});
service.setOnCancelled(e -> {
setProperties(false, true);
service.reset();
});
service.setOnFailed(e -> {
setProperties(false, true);
System.err.println("Copy failed: " + service.getException());
service.reset();
});
// Bind label text property to service
lblProgress.textProperty().bind(service.messageProperty());
btnCancel.setDisable(true);
}
// Method to disable/enable buttons and set label's text from events
private void setProperties(boolean disableStart, boolean disableCancel)
{
btnStart.setDisable(disableStart);
btnCancel.setDisable(disableCancel);
}
}
From the controller we use the service methods start, cancel and reset. start() launches the service when it is in the READY state. cancel() requests cancellation of the current task. Once a service has reached a terminal state (SUCCEEDED, FAILED or CANCELLED), it can be returned to READY with reset(), or cancelled-and-started again with restart() when that behavior is appropriate.
The handlers registered with setOnSucceeded, setOnCancelled and setOnFailed run on the JavaFX Application Thread, so they can safely update the UI. In the example, each handler resets the service so the Start button can launch it again.
We have also added a binding from the label text property to the service message property:
lblProgress.textProperty().bind(service.messageProperty());
Thanks to this binding, calls to updateMessage from the background Task are reflected in the label without explicitly calling Platform.runLater. If the service is reset, its worker properties return to their initial state, so the bound label may become empty again.
The service also exposes properties such as titleProperty, valueProperty, progressProperty, runningProperty, stateProperty and exceptionProperty. They can be observed or bound to controls, which is especially useful when several UI elements must reflect the same background operation.
However, while a writable property is bound, it cannot be assigned independently with its setter. For example, while lblProgress.textProperty() is bound to service.messageProperty(), calling lblProgress.setText(...) attempts to modify a bound property and is not valid. If direct manual changes are required, first unbind the property, update it, and bind it again when appropriate.
The return value of the service is used inside the setOnSucceeded method. When the service finishes properly, it will return a String with the text “Copy completed”. We can check this in the standard output, thanks to this line of code:
System.out.println(service.getValue());
The setProperties method is used from some events to update the “disable” state of both buttons (when we Start the copy, we disable the Start button, for instance), and the label text.
Exercise 2:
Create a project called My3CountersService, that will be a copy of project My3Counters from exercise 1. In this case, you must use a
Serviceto implement the 3 tasks. As soon as a given count finishes, the corresponding button must turn enabled, and we will be able to start it again.HELP: You must implement a void Service. As Service is a parameterized class, when you want it to return a void result you must use the
<Void>parameter. In thecallmethod, it must return aVoidtype, and you can do it by using areturn nullinstruction at the end of the method.
If we want to execute background work repeatedly, ScheduledService is a JavaFX-oriented alternative to manually writing an endless loop with sleep() inside a Service.
A ScheduledService creates a new Task for each execution. After a successful execution, it schedules the next one automatically until the service is cancelled.
Two timing properties are especially important:
delay: the initial delay before the first execution after start() or restart().period: the minimum time between the start of one execution and the start of the next one.This last detail is important. period is not simply a delay that begins after the previous task finishes. If a task finishes before the period expires, the service waits for the remaining time. If a task itself takes longer than the configured period, the next execution can start as soon as the previous one has completed.
A ScheduledService can also be configured to retry failed executions using properties such as restartOnFailure, maximumFailureCount and a backoff strategy.
Let’s see a simple example. We will create an executor with 20 tasks and use a ScheduledService to monitor it periodically:
public class FXServiceExampleController implements Initializable
{
@FXML
private Button button;
@FXML
private Label threadsPending;
@FXML
private Label threadsFinished;
private ScheduledService<Boolean> schedServ;
private ThreadPoolExecutor executor;
@Override
public void initialize(URL url, ResourceBundle rb)
{
schedServ = new ScheduledService<Boolean>()
{
@Override
protected Task<Boolean> createTask()
{
return new Task<Boolean>()
{
@Override
protected Boolean call()
{
int queuedTasks = executor.getQueue().size();
long finishedTasks = executor.getCompletedTaskCount();
Platform.runLater(() -> {
threadsPending.setText(
"Queued tasks: " + queuedTasks);
threadsFinished.setText(
"Finished tasks: " + finishedTasks);
});
return executor.isTerminated();
}
};
}
};
schedServ.setDelay(Duration.millis(500)); // Initial delay: 0.5 s
schedServ.setPeriod(Duration.seconds(1)); // Minimum start-to-start period
schedServ.setOnSucceeded(e -> {
if (schedServ.getValue())
{
// Executor has finished all its tasks
schedServ.cancel();
button.setDisable(false);
}
});
}
@FXML
private void startThreads(ActionEvent event)
{
button.setDisable(true);
int processors = Runtime.getRuntime().availableProcessors();
executor = new ThreadPoolExecutor(
processors,
processors,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>()
);
for (int i = 0; i < 20; i++)
{
executor.execute(() -> {
Random rnd = new Random();
try
{
TimeUnit.MILLISECONDS.sleep(
500 + rnd.nextInt(5000));
}
catch (InterruptedException ex)
{
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
schedServ.restart();
}
}
The code above starts an executor with 20 tasks that take between about 0.5 and 5.5 seconds to complete. The executor uses a fixed number of worker threads based on the number of available processors; it does not create 20 threads just because 20 tasks were submitted.
The ScheduledService starts after 0.5 seconds and then monitors the executor repeatedly. Its background Task reads the executor state, while Platform.runLater performs the corresponding label updates on the JavaFX Application Thread. When the executor reaches the terminated state, the scheduled service is cancelled and the button is enabled again.
For this particular example, another valid design would be to return a small status object from the Task and update both labels in setOnSucceeded, because worker-state event handlers already run on the JavaFX Application Thread.
Exercise 3:
Create a project called ScheduledChronometer. Create a view with a TextField where you’ll write a number of seconds and Start and Pause buttons.
Use a
ScheduledService<Integer>whose task returns the next value of the countdown. Configure it with a period of one second.
- When Start is pressed for the first time, start the service.
- When the service succeeds in one iteration, show the returned value in the interface.
- When the countdown reaches 0, cancel the service.
- JavaFX
ScheduledServicehas nopause()operation. Therefore, when Pause is pressed, cancel the service and store the last value in a field.- When Start is pressed again after a pause, restart the service using that stored value. Remember that a cancelled
Serviceis not in theREADYstate, so userestart()orreset()followed bystart()as appropriate.This exercise is useful for practising
ScheduledService. In a real application, if the only goal is to update a visual chronometer and no background work is required, a JavaFXTimelinecan be simpler. Remember thatTimelineitself runs on the JavaFX Application Thread.
You can download here the source code of some examples shown in this document.