JavaFX Concurrency Example
This is a JavaFX Concurrency Example. Java 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 JavaFX Application Thread, to process all UI events.
The following table shows an overview of the whole article:
Table Of Contents
The following examples uses Java SE 8 and JavaFX 2.2.
1. Introduction
The nodes representing 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 following examples will show the problem in detail.
2. The given Problem
2.1 The Code
FxConcurrencyExample1.java
import javafx.application.Application; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class FxConcurrencyExample1 extends Application { // Create the TextArea TextArea textArea = new TextArea(); // Create the Label Label statusLabel = new Label("Not Started..."); // Create the Buttons Button startButton = new Button("Start"); Button exitButton = new Button("Exit"); public static void main(String[] args) { Application.launch(args); } @Override public void start(final Stage stage) { // Create the Event-Handlers for the Buttons startButton.setOnAction(new EventHandler <ActionEvent>() { public void handle(ActionEvent event) { runTask(); } }); exitButton.setOnAction(new EventHandler <ActionEvent>() { public void handle(ActionEvent event) { stage.close(); } }); // Create the ButtonBox HBox buttonBox = new HBox(5, startButton, exitButton); // Create the VBox VBox root = new VBox(10, statusLabel, buttonBox, textArea); // Set the Style-properties of the VBox root.setStyle("-fx-padding: 10;" + "-fx-border-style: solid inside;" + "-fx-border-width: 2;" + "-fx-border-insets: 5;" + "-fx-border-radius: 5;" + "-fx-border-color: blue;"); // Create the Scene Scene scene = new Scene(root,400,300); // Add the scene to the Stage stage.setScene(scene); // Set the title of the Stage stage.setTitle("A simple Concurrency Example"); // Display the Stage stage.show(); } public void runTask() { for(int i = 1; i <= 10; i++) { try { String status = "Processing " + i + " of " + 10; statusLabel.setText(status); textArea.appendText(status+"\n"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
The above program displays a window as shown in the GUI. 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
The program is very simple. When you click the Start Button, a task lasting for 10 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 1 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 10 seconds for the task to finish.
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 don´t see the status updated in the Label
.
public void runTask() { for(int i = 1; i <= 10; i++) { try { String status = "Processing " + i + " of " + 10; statusLabel.setText(status); textArea.appendText(status+"\n"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
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
.
2.2 The GUI
The following image shows the GUI after clicking the Start Button
:
The following image shows the GUI of the above program after execution:
3. A first Solution Approach
3.1 The Code
FxConcurrencyExample2.java
import javafx.application.Application; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class FxConcurrencyExample2 extends Application { // Create the TextArea TextArea textArea = new TextArea(); // Create the Label Label statusLabel = new Label("Not Started..."); // Create the Buttons Button startButton = new Button("Start"); Button exitButton = new Button("Exit"); public static void main(String[] args) { Application.launch(args); } @Override public void start(final Stage stage) { // Create the Event-Handlers for the Buttons startButton.setOnAction(new EventHandler <ActionEvent>() { public void handle(ActionEvent event) { startTask(); } }); exitButton.setOnAction(new EventHandler <ActionEvent>() { public void handle(ActionEvent event) { stage.close(); } }); // Create the ButtonBox HBox buttonBox = new HBox(5, startButton, exitButton); // Create the VBox VBox root = new VBox(10, statusLabel, buttonBox,textArea); // Set the Style-properties of the VBox root.setStyle("-fx-padding: 10;" + "-fx-border-style: solid inside;" + "-fx-border-width: 2;" + "-fx-border-insets: 5;" + "-fx-border-radius: 5;" + "-fx-border-color: blue;"); // Create the Scene Scene scene = new Scene(root,400,300); // Add the scene to the Stage stage.setScene(scene); // Set the title of the Stage stage.setTitle("A simple Concurrency Example"); // Display the Stage stage.show(); } public void startTask() { // Create a Runnable Runnable task = new Runnable() { public void run() { 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; statusLabel.setText(status); textArea.appendText(status+"\n"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
The above program 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.
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:236) at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:423) at javafx.scene.Parent$2.onProposedChange(Parent.java:367) at com.sun.javafx.collections.VetoableListDecorator.setAll(VetoableListDecorator.java:113) at com.sun.javafx.collections.VetoableListDecorator.setAll(VetoableListDecorator.java:108) at com.sun.javafx.scene.control.skin.LabeledSkinBase.updateChildren(LabeledSkinBase.java:575) at com.sun.javafx.scene.control.skin.LabeledSkinBase.handleControlPropertyChanged(LabeledSkinBase.java:204) at com.sun.javafx.scene.control.skin.LabelSkin.handleControlPropertyChanged(LabelSkin.java:49) at com.sun.javafx.scene.control.skin.BehaviorSkinBase.lambda$registerChangeListener$61(BehaviorSkinBase.java:197) at com.sun.javafx.scene.control.MultiplePropertyChangeListenerHandler$1.changed(MultiplePropertyChangeListenerHandler.java:55) at javafx.beans.value.WeakChangeListener.changed(WeakChangeListener.java:89) at com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:182) at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81) at javafx.beans.property.StringPropertyBase.fireValueChangedEvent(StringPropertyBase.java:103) at javafx.beans.property.StringPropertyBase.markInvalid(StringPropertyBase.java:110) at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:144) at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:49) at javafx.beans.property.StringProperty.setValue(StringProperty.java:65) at javafx.scene.control.Labeled.setText(Labeled.java:145) at FXConcurrency.FxConcurrencyExample2.runTask(FxConcurrencyExample2.java:101) at FXConcurrency.FxConcurrencyExample2$3.run(FxConcurrencyExample2.java:82) at java.lang.Thread.run(Thread.java:745)
The following statement in the runTask()
method generated the exception:
statusLabel.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.
3.2 The GUI
The following image shows the result of the program during execution:
4. The Solution
4.1 The Code
FxConcurrencyExample3.java
import javafx.application.Application; import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class FxConcurrencyExample3 extends Application { // Create the TextArea TextArea textArea = new TextArea(); // Create the Label Label statusLabel = new Label("Not Started..."); // Create the Buttons Button startButton = new Button("Start"); Button exitButton = new Button("Exit"); public static void main(String[] args) { Application.launch(args); } @Override public void start(final Stage stage) { // Create the Event-Handlers for the Buttons startButton.setOnAction(new EventHandler <ActionEvent>() { public void handle(ActionEvent event) { startTask(); } }); exitButton.setOnAction(new EventHandler <ActionEvent>() { public void handle(ActionEvent event) { stage.close(); } }); // Create the ButtonBox HBox buttonBox = new HBox(5, startButton, exitButton); // Create the VBox VBox root = new VBox(10, statusLabel, buttonBox, textArea); // Set the Style-properties of the VBox root.setStyle("-fx-padding: 10;" + "-fx-border-style: solid inside;" + "-fx-border-width: 2;" + "-fx-border-insets: 5;" + "-fx-border-radius: 5;" + "-fx-border-color: blue;"); // Create the Scene Scene scene = new Scene(root,400,300); // Add the scene to the Stage stage.setScene(scene); // Set the title of the Stage stage.setTitle("A simple Concurrency Example"); // Display the Stage stage.show(); } public void startTask() { // Create a Runnable Runnable task = new Runnable() { public void run() { 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 { // Get the Status final String status = "Processing " + i + " of " + 10; // Update the Label on the JavaFx Application Thread Platform.runLater(new Runnable() { @Override public void run() { statusLabel.setText(status); } }); textArea.appendText(status+"\n"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
Let us fix the problem in the application. The above program is the correct implementation of the logic to access the live Scene
Graph.
The program replaces the statement
statusLabel.setText(status);
in the Java class with the statement
// Update the Label on the JavaFx Application Thread Platform.runLater(new Runnable() { @Override public void run() { statusLabel.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 a 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 a 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.
I will discuss the framework more in detail in a special article.
4.2 The GUI
The following GUI shows a snapshot of the window displayed by the program.
5. Download Java Source Code
This was an example of javafx.concurrent
You can download the full source code of this example here: JavaFxConcurrencyExample.zip
Excellent article!
Thank you! I am very grateful to you. I’m tried to find a solution to such a problem for a long time. Your article is excellent.
Hi, Thanks for the post. I was wondering if there is a way, if we want to make different threads in the same class, for instance, I want to run different exe files using different threads and all the threads have to wait for the first completion. Can we make multiple Start task() and then write run() for different threads.
Hi, nice article, why does the line : statusLabel.setText(status); raises and exception but textArea.appendText(status+”\n”); does not?
Shouldn’t we be updating both the label and the textArea from the JavaFX UI thread?
Thanks
Paulo
This article saved my day
The final solution is incorrect; the call to
textArea.appendText(status+”\n”);
should also be executed inside thePlatform.runLater(...)
runnable block. (Text areas do not perform thread checks, however they are still not thread safe, so while this doesn’t throw an Exception, it has the potential to leave the text area in an inconsistent state.)