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