我的应用程序包含一个 ListView
每次选择项目时都会启动后台任务。后台任务成功完成后会更新 UI 上的信息。
然而,当用户快速点击一个又一个项目时,所有这些任务都会继续,最后一个完成的任务“获胜”并更新 UI,无论最后选择哪个项目。
我需要以某种方式确保此任务在任何给定时间只运行一个实例,因此在开始新任务之前取消所有先前的任务。
这是演示该问题的 MCVE:
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();
}
}
当以随机顺序单击多个项目时,结果是不可预测的。我需要的是更新标签以显示最后一次
Task
的结果那被执行了。我是否需要以某种方式安排任务或将它们添加到服务中以取消所有先前的任务?
编辑:
在我的实际应用程序中,用户从
ListView
中选择一个项目。后台任务读取数据库(一个复杂的 SELECT
语句)以获取与该项目关联的所有信息。然后这些详细信息会显示在应用程序中。发生的问题是,当用户选择一个项目但更改了他们的选择时,应用程序中显示的返回数据可能是所选的第一个项目,即使现在选择了一个完全不同的项目。
从第一个(即:不需要的)选择返回的任何数据都可以完全丢弃。
最佳答案
您的要求,为this answer提到,似乎是使用 Service
的完美理由.一个 Service
允许您运行一个 Task
在任何给定时间以可重复使用的方式1。当您取消 Service
, 通过 Service.cancel()
,它取消了基础Task
. Service
还跟踪自己的Task
为您服务,因此您无需将它们保存在某个列表中。
使用您的 MVCE 您想要做的是创建一个 Service
包裹您的 Task
.每次用户在 ListView
中选择一个新项目时你会取消 Service
,更新必要的状态,然后重新启动 Service
.然后你会使用 Service.setOnSucceeded
回调将结果设置为 Label
.这保证只有最后一次成功的执行才会返回给您。即使之前取消 Task
s 仍然返回结果 Service
会忽略它们。
您也无需担心外部同步(至少在您的 MVCE 中)。所有处理启动、取消和观察 Service
的操作发生在 FX 线程上。唯一没有在 FX 线程上执行的代码(如下所示)将在 Task.call()
中。 (好吧,以及当类被实例化时立即分配的字段,我相信这发生在 JavaFX-Launcher 线程上)。
这是使用 Service
的 MVCE 的修改版本:
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.");
}
}
}
当您拨打
Service.start()
它创建了一个 Task
并使用当前 Executor
执行它包含在其 executor property 中.如果该属性包含 null
然后它使用一些未指定的默认值 Executor
(使用守护线程)。上面,你看我打电话
reset()
取消后在onSucceeded
和 onFailed
回调。这是因为 Service
只能在 READY
中启动state .您可以使用 restart()
而不是 start()
如果需要的话。基本上相当于调用cancel()
-> reset()
-> start()
.1
Task
不会变成可恢复的。相反,Service
创建一个新的 Task
每次启动。当您取消
Service
它取消当前正在运行的 Task
,如果有的话。即使 Service
,因此 Task
, 已取消 并不意味着执行实际上已经停止 .在 Java 中,取消后台任务需要与该任务的开发者合作。这种合作采取定期检查执行是否应该停止的形式。如果使用普通
Runnable
或 Callable
这将需要检查当前 Thread
的中断状态或使用一些 boolean
标志2。自 Task
扩展 FutureTask
您也可以使用 isCancelled()
方法继承自 Future
界面。如果您不能使用 isCancelled()
出于某种原因(称为外部代码,不使用 Task
等...)然后您使用以下方法检查线程中断:Thread.interrupted()
Thread
Thread.isInterrupted()
Thread
您可以引用 您可以通过
Thread.currentThread()
获得对当前线程的引用。 .在您的后台代码中,如果当前线程已被中断,您需要在适当的点进行检查,
boolean
标志已设置,或 Task
已经取消了。如果有,那么您将执行任何必要的清理并停止执行(通过返回或抛出异常)。另外,如果您的
Thread
正在等待一些可中断的操作,比如阻塞IO,那么它会抛出一个InterruptedException
中断时。在您的 MVCE 中,您使用 Thread.sleep
可中断;意思是当你调用取消这个方法时会抛出提到的异常。当我在上面说“清理”时,我的意思是在后台进行任何必要的清理,因为您仍在后台线程上。如果您需要清理 FX 线程上的任何内容(例如更新 UI),那么您可以使用
onCancelled
Service
的属性(property).在上面的代码中,您还会看到我使用了 protected 方法
succeeded()
和 cancelled()
.两者 Task
和 Service
提供这些方法(以及用于各种 Worker.State
的其他方法),它们将始终在 FX 线程上调用。但是,请阅读文档,如 ScheduledService
要求您为其中一些方法调用 super 实现。2如果使用
boolean
标志确保其他线程可以看到它的更新。你可以通过制作它来做到这一点 volatile
,同步它,或使用 java.util.concurrent.atomic.AtomicBoolean
.
关于java - 如果启动了新的任务实例,如何取消任务?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51312677/