My application contains a ListView
that kicks off a background task every time an item is selected. The background task then updates information on the UI when it completes successfully.
However, when the user quickly clicks one item after another, all these tasks continue and the last task to complete "wins" and updates the UI, regardless of which item was selected last.
What I need is to somehow ensure this task only has one instance of it running at any given time, so cancel all prior tasks before starting the new one.
Here is an MCVE that demonstrates the issue:
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class taskRace extends Application {
private final ListView<String> listView = new ListView<>();
private final Label label = new Label("Nothing selected");
private String labelValue;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
// Simple UI
VBox root = new VBox(5);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
root.getChildren().addAll(listView, label);
// Populate the ListView
listView.getItems().addAll(
"One", "Two", "Three", "Four", "Five"
);
// Add listener to the ListView to start the task whenever an item is selected
listView.getSelectionModel().selectedItemProperty().addListener((observableValue, oldValue, newValue) -> {
if (newValue != null) {
// Create the background task
Task task = new Task() {
@Override
protected Object call() throws Exception {
String selectedItem = listView.getSelectionModel().getSelectedItem();
// Do long-running task (takes random time)
long waitTime = (long)(Math.random() * 15000);
System.out.println("Waiting " + waitTime);
Thread.sleep(waitTime);
labelValue = "You have selected item: " + selectedItem ;
return null;
}
};
// Update the label when the task is completed
task.setOnSucceeded(event ->{
label.setText(labelValue);
});
new Thread(task).start();
}
});
stage.setScene(new Scene(root));
stage.show();
}
}
When clicking on several items in random order, the outcome is unpredictable. What I need is for the label to be updated to show the results from the last Task
that was executed.
Do I need to somehow schedule the tasks or add them to a service in order to cancel all prior tasks?
EDIT:
In my real world application, the user selects an item from the ListView
and the background task reads a database ( a complex SELECT
statement) to get all the information associated with that item. Those details are then displayed in the application.
The issue that is happening is when the user selects an item but changes their selection, the data returned displayed in the application could be for the first item selected, even though a completely different item is now chosen.
Any data returned from the first (ie: unwanted) selection can be discarded entirely.
Create and start a cancelable task. Pass a cancellation token to your user delegate and optionally to the task instance. Notice and respond to the cancellation request in your user delegate. Optionally notice on the calling thread that the task was canceled.
Once cancelled, you'll be 100% fully refunded automatically and you can then accept another Taskers offer. Refunds can take anywhere between a couple of hours to 7 business days to reach your account depending on your bank.
You can cancel an asynchronous operation after a period of time by using the CancellationTokenSource. CancelAfter method if you don't want to wait for the operation to finish.
Your requirements, as this answer mentions, seem like a perfect reason for using a Service
. A Service
allows you to run one Task
at any given time in a reusable1 manner. When you cancel a Service
, via Service.cancel()
, it cancels the underlying Task
. The Service
also keeps track of its own Task
for you so you don't need to keep them in a list somewhere.
Using your MVCE what you'd want to do is create a Service
that wraps your Task
. Every time the user selects a new item in the ListView
you'd cancel the Service
, update the necessary state, and then restart the Service
. Then you'd use the Service.setOnSucceeded
callback to set the result to the Label
. This guarantees that only the last successful execution will be returned to you. Even if previously cancelled Task
s still return a result the Service
will ignore them.
You also don't need to worry about external synchronization (at least in your MVCE). All the operations that deal with starting, cancelling, and observing the Service
happen on the FX thread. The only bit of code (featured below) not executed on the FX thread will be inside Task.call()
(well, and the immediately assigned fields when the class gets instantiated which I believe happens on the JavaFX-Launcher thread).
Here is a modified version of your MVCE using a Service
:
import javafx.application.Application;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
private final ListView<String> listView = new ListView<>();
private final Label label = new Label("Nothing selected");
private final QueryService service = new QueryService();
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
service.setOnSucceeded(wse -> {
label.setText(service.getValue());
service.reset();
});
service.setOnFailed(wse -> {
// you could also show an Alert to the user here
service.getException().printStackTrace();
service.reset();
});
// Simple UI
VBox root = new VBox(5);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
root.getChildren().addAll(listView, label);
// Populate the ListView
listView.getItems().addAll(
"One", "Two", "Three", "Four", "Five"
);
listView.getSelectionModel().selectedItemProperty().addListener((observableValue, oldValue, newValue) -> {
if (service.isRunning()) {
service.cancel();
service.reset();
}
service.setSelected(newValue);
service.start();
});
stage.setScene(new Scene(root));
stage.show();
}
private static class QueryService extends Service<String> {
// Field representing a JavaFX property
private String selected;
private void setSelected(String selected) {
this.selected = selected;
}
@Override
protected Task<String> createTask() {
return new Task<>() {
// Task state should be immutable/encapsulated
private final String selectedCopy = selected;
@Override
protected String call() throws Exception {
try {
long waitTime = (long) (Math.random() * 15_000);
System.out.println("Waiting " + waitTime);
Thread.sleep(waitTime);
return "You have selected item: " + selectedCopy;
} catch (InterruptedException ex) {
System.out.println("Task interrupted!");
throw ex;
}
}
};
}
@Override
protected void succeeded() {
System.out.println("Service succeeded.");
}
@Override
protected void cancelled() {
System.out.println("Service cancelled.");
}
}
}
When you call Service.start()
it creates a Task
and executes it using the current Executor
contained in its executor property. If the property contains null
then it uses some unspecified, default Executor
(using daemon threads).
Above, you see I call reset()
after cancelling and in the onSucceeded
and onFailed
callbacks. This is because a Service
can only be started when in the READY
state. You can use restart()
rather than start()
if needed. It is basically equivalent to calling cancel()
->reset()
->start()
.
1The Task
doesn't become resuable. Rather, the Service
creates a new Task
each time it's started.
When you cancel a Service
it cancels the currently running Task
, if there is one. Even though the Service
, and therefore Task
, have been cancelled does not mean that execution has actually stopped. In Java, cancelling background tasks requires cooperation with the developer of said task.
This cooperation takes the form of periodically checking if execution should stop. If using a normal Runnable
or Callable
this would require checking the interrupted status of the current Thread
or using some boolean
flag2. Since Task
extends FutureTask
you can also use the isCancelled()
method inherited from the Future
interface. If you can't use isCancelled()
for some reason (called outside code, not using a Task
, etc...) then you check for thread interruption using:
Thread.interrupted()
Thread
Thread.isInterrupted()
Thread
you have a reference toYou can get a reference to the current thread via Thread.currentThread()
.
Inside your background code you'd want to check at appropriate points if the current thread has been interrupted, the boolean
flag has been set, or the Task
has been cancelled. If it has then you'd perform any necessary clean up and stop execution (either by returning or throwing an exception).
Also, if your Thread
is waiting on some interruptible operation, such as blocking IO, then it will throw an InterruptedException
when interrupted. In your MVCE you use Thread.sleep
which is interruptible; meaning when you call cancel this method will throw the mentioned exception.
When I say "clean up" above I mean any clean up necessary in the background since you're still on the background thread. If you need to clean up anything on the FX thread (such as updating the UI) then you can use the onCancelled
property of Service
.
In the code above you'll also see I use the protected methods succeeded()
and cancelled()
. Both Task
and Service
provide these methods (as well as others for the various Worker.State
s) and they will always be called on the FX thread. Read the documentation, however, as ScheduledService
requires you to call the super implementations for some of these methods.
2If using a boolean
flag make sure updates to it are visible to other threads. You can do this by making it volatile
, synchronizing it, or using a java.util.concurrent.atomic.AtomicBoolean
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With