I'm building a GUI in JavaFX for a rather large Java project. This project has many different worker threads doing some heavy computations in the background and I'm trying to visualize the progress of these worker threads in a GUI. By progress I mean not only a bare percentage but also other variables not contained within the Task class such as (for example):
- The current file
- The current Error count
- The amount of bytes read so far
- ...
As these progress variables change very rapidly and because I have to do the GUI updates from the JavaFX thread (Platform.runLater()), the JavaFX event queue gets overloaded very quickly. I'm trying to fix this by building a utility class capable of asynchronously updating GUI Properties from outside the JavaFX threads. Rapid consecutive updates should be skipped in order to only display the latest value and thus avoid swarming the JavaFX event queue with Runnables.
I therefore built the following class GUIUpdater
to bind Properties (Usually a GUI element such as a Label) to ObservableValues (like a SimpleStringProperty). This class has two InnerClasses:
PropertyUpdater
is responsible for binding a single Property to a single ObservableValue and updating it.- The
Updater
provides a re-usable Runnable object for Platform.runLater().
The utility class:
package main;
import java.util.concurrent.ConcurrentLinkedQueue;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
/**
* Class for enabling fast updates of GUI components from outside the JavaFX thread.
* Updating GUI components (such as labels) should be done from the JavaFX thread by using Platform.runLater for example.
* This makes it hard to update the GUI with a fast changing variable as it is very easy to fill up the JavaFX event queue faster than it can be emptied (i.e. faster than it can be drawn).
* This class binds ObservableValues to (GUI) Properties and ensures that quick consecutive updates are ignored, only updating to the latest value.
*/
public class GUIUpdater {
private ConcurrentLinkedQueue<PropertyUpdater<?>> dirtyPropertyUpdaters = new ConcurrentLinkedQueue<>();
private Updater updater = new Updater();
private boolean isUpdating = false;
/**
* Binds an ObservableValue to a Property.
* Updates to the ObservableValue can be made from outside the JavaFX thread and the latest update will be reflected in the Property.
* @param property (GUI) Property to be updated/
* @param observable ObservableValue to update the GUI property to.
*/
public <T> void bind(Property<T> property, ObservableValue<T> observable) {
PropertyUpdater<T> propertyUpdater = new PropertyUpdater<>(property, observable);
observable.addListener(propertyUpdater);
}
/**
* Unbinds the given ObservableValue from the given Property.
* Updates to the ObservableValue will no longer be reflected in the Property.
* @param property (GUI) Property to unbind the ObservableValue from.
* @param observable ObservableValue to unbind from the given Property.
*/
public <T> void unbind(Property<T> property, ObservableValue<T> observable) {
PropertyUpdater<T> tmpPropertyUpdater = new PropertyUpdater<>(property, observable);
observable.removeListener(tmpPropertyUpdater);
}
/**
* Schedules an update to the GUI by using a call to Platform.runLater().
* The updated property is added to the dirtyProperties list, marking it for the next update round.
* Will only submit the event to the event queue if the event isn't in the event queue yet.
* @param updater
*/
private void scheduleUpdate(PropertyUpdater<?> updater) {
this.dirtyPropertyUpdaters.add(updater);
// Make sure the isUpdating var isn't changed concurrently by the Updater thread (on the JavaFX event queue)
synchronized (this) {
if (!this.isUpdating) {
this.isUpdating = true;
Platform.runLater(this.updater);
}
}
}
/**
* Class used for binding a single ObservableValue to a Property and updating it.
*
* @param <T>
*/
private class PropertyUpdater<T> implements ChangeListener<T> {
private boolean isDirty = false;
private Property<T> property = null;
private ObservableValue<T> observable = null;
public PropertyUpdater(Property<T> property, ObservableValue<T> observable) {
this.property = property;
this.observable = observable;
}
@Override
/**
* Called whenever the ObservableValue has changed. Marks this Updater as dirty.
*/
public synchronized void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) {
if (!this.isDirty) {
this.isDirty = true;
GUIUpdater.this.scheduleUpdate(this);
}
}
/**
* Updates the Property to the ObservableValue and marks it as clean again.
* Should only be called from the JavaFX thread.
*/
public synchronized void update() {
T value = this.observable.getValue();
this.property.setValue(value);
this.isDirty = false;
}
@Override
/**
* Two PropertyUpdaters are equals if their Property and ObservableValue map to the same object (address).
*/
public boolean equals(Object otherObj) {
PropertyUpdater<?> otherUpdater = (PropertyUpdater<?>) otherObj;
if (otherObj == null) {
return false;
} else {
// Only compare addresses (comparing with equals also compares contents):
return (this.property == otherUpdater.property) && (this.observable == otherUpdater.observable);
}
}
}
/**
* Simple class containing the Runnable for the call to Platform.runLater.
* Hence, the run() method should only be called from the JavaFX thread.
*
*/
private class Updater implements Runnable {
@Override
public void run() {
// Loop through the individual PropertyUpdaters, updating them one by one:
while(!GUIUpdater.this.dirtyPropertyUpdaters.isEmpty()) {
PropertyUpdater<?> curUpdater = GUIUpdater.this.dirtyPropertyUpdaters.poll();
curUpdater.update();
}
// Make sure we're not clearing the mark when scheduleUpdate() is still setting it:
synchronized (GUIUpdater.this) {
GUIUpdater.this.isUpdating = false;
}
}
}
}
And this is a simple class for testing the GUIUpdater
utility class:
package main;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Task;
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.ProgressBar;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class JavaFXTest extends Application {
private GUIUpdater guiUpdater = new GUIUpdater();
private Label lblState = new Label();
private ProgressBar prgProgress = new ProgressBar();
public static void main(String args[]) {
JavaFXTest.launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
// Init window:
FlowPane flowPane = new FlowPane();
primaryStage.setScene(new Scene(flowPane));
primaryStage.setTitle("JavaFXTest");
// Add a Label and a progressBar:
flowPane.getChildren().add(this.lblState);
flowPane.getChildren().add(this.prgProgress);
// Add button:
Button btnStart = new Button("Start");
btnStart.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
// Create task:
TestTask testTask = new TestTask();
// Bind:
JavaFXTest.this.guiUpdater.bind(JavaFXTest.this.lblState.textProperty(), testTask.myStateProperty());
JavaFXTest.this.prgProgress.progressProperty().bind(testTask.progressProperty()); // No need to use GUIUpdater here, Task class provides the same functionality for progress.
// Start task:
Thread tmpThread = new Thread(testTask);
tmpThread.start();
}
});
flowPane.getChildren().add(btnStart);
// Show:
primaryStage.show();
}
/**
* A simple task containing a for loop to simulate a fast running and fast updating process.
* @author DePhille
*
*/
private class TestTask extends Task<Void> {
private SimpleStringProperty myState = new SimpleStringProperty();
@Override
protected Void call() throws Exception {
// Count:
try {
int maxValue = 1000000;
System.out.println("Starting...");
for(int i = 0; i < maxValue; i++) {
this.updateProgress(i, maxValue - 1);
this.myState.set("Hello " + i);
}
System.out.println("Done!");
} catch(Exception e) {
e.printStackTrace();
}
// Unbind:
JavaFXTest.this.guiUpdater.unbind(JavaFXTest.this.lblState.textProperty(), this.myStateProperty());
return null;
}
public SimpleStringProperty myStateProperty() {
return this.myState;
}
}
}
The problem with the code is that sometimes the Label is not updated to the latest value (in this case 999999). It seems to mostly happen right after the Application is started, so starting the app, clicking the "Start" button, closing it and repeating this process should replicate the problem after a few tries. As far as I can see I've added synchronized
blocks where needed which is why I don't understand where the problem comes from.
Even though I'm primarily looking for the solution to the described problem all suggestions are very much appreciated (even those not related to the problem)! I added comments in the code as well, so I hope that together with the information above this provides enough detail about the problem and the code.
Thanks in advance!
I believe this functionality can be achieved through the
Task
'smessageProperty
:I was able to fix the issue myself. After a few days of adding
System.out
in various places it turned out the problem was due to concurrency issues with theisUpdating
variable. The problem occured when the JavaFX thread was inbetween thewhile
loop and thesynchronized
block inUpdater.run
. I solved the problem by making both theUpdater.run
andGUIUpdater.scheduleUpdate
methods synchronized on the same object.I also made the
GUIUpdater
into a static-only object as having multiple instances will placeRunnables
in the JavaFX event queue regardless of the otherGUIUpdater
instances, clogging up the event queue. All in all, this is the resultingGUIUpdater
class:And this is a slightly updated version of the tester class (I'm not going into detail on this one as it's totally unimportant):