我的应用程序包含一个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
语句)以获取与该项目相关的所有信息。然后,这些详细信息将显示在应用程序中。
发生的问题是,当用户选择一个项目但更改了选择时,即使现在选择了一个完全不同的项目,应用程序中显示的返回数据也可能是第一个选择的项目。
从第一个(即不需要的)选择返回的任何数据都可以完全丢弃。
答案 0 :(得分:3)
如this answer所述,您的要求似乎是使用Service
的完美理由。 Service
允许您以可重用的 1 方式在任何给定时间运行 one Task
。当您通过Service
取消Service.cancel()
时,它将取消基础Task
。 Service
还会为您跟踪自己的Task
,因此您无需将它们保存在列表中。
使用您的MVCE,您想要做的是创建一个Service
,将您的Task
包裹起来。每次用户在ListView
中选择新项目时,您都会取消Service
,更新必要的状态,然后重新启动Service
。然后,您将使用Service.setOnSucceeded
回调将结果设置为Label
。这样可以确保仅将最后一次成功执行返回给您。即使先前取消的Task
仍然返回结果,Service
也会忽略它们。
您也不必担心外部同步(至少在MVCE中)。处理开始,取消和观察Service
的所有操作都在FX线程上进行。在FX线程上执行的唯一代码(如下所示) not 将在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 property中包含的当前Executor
来执行。如果该属性包含null
,则它使用一些未指定的默认Executor
(使用守护程序线程)。
以上,您看到取消后在onSucceeded
和onFailed
回调中我调用reset()
。这是因为Service
仅在READY
state中时才能启动。如果需要,可以使用restart()
而不是start()
。基本上等同于调用cancel()
-> reset()
-> start()
。
1 Task
不可恢复。相反,Service
每次启动时都会创建一个新的Task
。
取消Service
时,它会取消当前运行的Task
(如果有)。即使Service
和Task
已被取消,并不意味着执行实际上已经停止。在Java中,取消后台任务需要与该任务的开发者合作。
这种合作采取定期检查执行是否停止的形式。如果使用普通的Runnable
或Callable
,则需要检查当前Thread
的中断状态或使用某些boolean
标志 2 。由于Task
扩展了FutureTask
,因此您还可以使用从Future
接口继承的isCancelled()
方法。如果由于某种原因无法使用isCancelled()
(称为外部代码,不使用Task
等),则可以使用以下方法检查线程中断:
Thread.interrupted()
Thread
Thread.isInterrupted()
Thread
您可以通过Thread.currentThread()
获得对当前线程的引用。
在后台代码中,您想在适当的位置检查当前线程是否已中断,boolean
标志是否已设置或Task
已被取消。如果有,那么您将执行任何必要的清除操作并停止执行(通过返回或引发异常)。
另外,如果您的Thread
正在等待某些可中断的操作,例如阻塞IO,则在中断时它将抛出InterruptedException
。在您的MVCE中,您可以使用Thread.sleep
,它是可中断的;意味着当您调用cancel时,此方法将引发所提到的异常。
当我在上面说“清除”时,是指在后台进行所有必要的 ,因为您仍在后台线程中。如果您需要清理FX线程上的所有内容(例如更新UI),则可以使用onCancelled
的{{1}}属性。
在上面的代码中,您还将看到我使用受保护的方法Service
和succeeded()
。 cancelled()
和Task
都提供了这些方法(以及各种Service
的其他方法),并且它们将始终在FX线程上被调用。但是,请阅读文档,因为Worker.State
要求您为其中某些方法调用超级实现。
2 如果使用ScheduledService
标志,请确保对其的更新对其他线程可见。您可以通过使其boolean
,进行同步或使用java.util.concurrent.atomic.AtomicBoolean
来实现。
答案 1 :(得分:0)
调用javafx.concurrent.Service.cancel()
将取消当前运行的Task
,并抛出InterruptedException
。
取消任何当前正在运行的任务(如果有)。该状态将设置为
CANCELLED
。
~ Service (JavaFX 8) - cancel ~
如果需要进行其他清理,则可以提供Task.onCancelled()
事件处理程序。
答案 2 :(得分:0)
演示类
package com.changehealthcare.pid;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadFun {
public static ExecutorService threadExecutor = Executors.newFixedThreadPool(1);
public static ServiceThread serviceThreads = new ServiceThread();
public static Future futureService;
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String line = "";
while (line.equalsIgnoreCase("quit") == false) {
line = in.readLine();
//do something
if (futureService != null) {
System.out.println("try to cancel");
futureService.cancel(true);
}
futureService = threadExecutor.submit(serviceThreads);
}
in.close();
}
}
ServiceThread类
package com.changehealthcare.pid;
public class ServiceThread implements Runnable {
@Override
public void run() {
System.out.println("starting the service");
boolean isResult = this.doStuff();
if (isResult) {
this.displayStuff();
}
System.out.println("done");
}
private boolean doStuff() {
try {
for (int i=0; i<=10; i++) {
System.out.println(i);
Thread.sleep(1000);
}
return true;
}
catch (InterruptedException e) {
System.out.println("cancelled");
return false;
}
}
private void displayStuff() {
System.out.println("display done");
}
}
说明 该演示应用程序充当System.in(键盘)的侦听器。如果按下任何字符(使用Enter键),它将取消当前存在的任何线程并提交新的线程。
服务线程演示有一个run()方法,该方法调用doStuff()和displayStuff()。您可以为displayStuff注入UI部件。在doStuff()中,它正在捕获InterupptedException,如果发生该异常,我们将不执行displayStuff()。
请检查并确定它是否适合您的情况。我们可以做更多的事情。
答案 3 :(得分:0)
满足您需求的最简单的代码是:
class SingleTaskRunner {
private Task<?> lastTask = null;
void runTask(Task<?> task) {
registerTask(task);
new Thread(task).start();
}
private synchronized void registerTask(Task<?> task) {
if (lastTask != null) {
lastTask.cancel(true);
}
lastTask = task;
}
}
请注意,它与Nghia Do's answer非常相似,只是具有同步,而没有所有这些额外的演示代码。
您评论说,您对所提到的答案如何允许您取消所有先前的任务感到困惑,但是事实是-正确同步时(例如registerTask
),您只需要存储最后一个任务因为所有先前的任务都将被取消(通过相同的registerTask
方法,由于已同步,因此将永远不会同时执行)。据我所知,在已经完成的cancel
上调用Task
是无操作的。
此外,在ExecutorService
上使用new Thread(task).start()
(如Nghia Do建议)也是一个好主意,因为Thread
creation is expensive。
编辑:很抱歉,我没有尝试过您的MCVE。现在,我已经无法再现行为,我注意到您的MCVE中存在以下缺陷:
您正在使用Thread.sleep()
模拟计算,但是在Thread.sleep()
内部调用Future.cancel()
会立即终止任务,因为Future.cancel()
calls Thread.interrupt()
在内部,并且{{1} }检查中断标志,您很可能在实际代码中没有这样做。
您正在将计算结果分配给私有字段。由于(如上所述),计算被立即在您的原始MCVE中中断,并且可能未被在您的真实代码中被中断,因此在您的真实代码中该字段的值可能会被已经取消的任务覆盖。
我相信在您的真实代码中,您可能所做的不只是将结果关联到现场。在原始的MCVE中,即使将Thread.sleep()
替换为循环后,我也无法重现该行为。原因是Thread.sleep()
处理程序在任务被取消时没有不被调用(很好)。不过,我相信,在您的真实代码中,您可能在onSucceeded
中做了比在MCVE中显示的更重要的事情(例如,某些GUI更新),因为据我所知,否则,您不会尚未遇到原始问题。
这是您的MCVE的修改,应该不显示原始问题。
EDIT :我进一步修改了MCVE,以打印取消信息以及事件的打印时间。但是,此代码不会重现OP编写的行为。
Task.call()
答案 4 :(得分:0)
如果由于某种原因,您无法使用基于取消的解决方案(参见my answer,Slaw's answer以及实际上所有其他答案)使它在实际用例中正常工作,则有您可以尝试另一件事。
从理论上讲,这里提出的解决方案比任何基于取消的解决方案都要更糟糕(因为所有冗余计算都将继续进行直到完成,从而浪费了资源)。但是,实际上,一个可行的解决方案将比不可行的解决方案更好(前提是它对您有效,并且唯一可以解决此问题-因此请先检查Slaw's solution)
我建议您完全放弃取消,而是使用一个简单的volatile字段来存储最后选择的项目的值:
private volatile String lastSelectedItem = null;
选择新标签后,您将更新此字段:
lastSelectedItem = task.selectedItem;
然后,在成功事件中,您只需检查是否允许分配计算结果:
if (task.selectedItem.equals(lastSelectedItem))
在下面找到整个修改后的MCVE:
public class TaskRaceWithoutCancelation extends Application {
private final ListView<String> listView = new ListView<>();
private final Label label = new Label("Nothing selected");
private final long startMillis = System.currentTimeMillis();
private volatile String lastSelectedItem = null;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) {
// 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
MyTask task = new MyTask();
lastSelectedItem = task.selectedItem;
// Update the label when the task is completed
task.setOnSucceeded(event -> {
if (task.selectedItem.equals(lastSelectedItem)) {
label.setText(task.getValue());
println("Assigned " + task.selectedItem);
}
});
new Thread(task).start();
}
});
stage.setScene(new Scene(root));
stage.show();
}
private void println(String string) {
System.out.format("%5.2fs: %s%n", 0.001 * (System.currentTimeMillis() - startMillis), string);
}
private class MyTask extends Task<String> {
final String selectedItem = listView.getSelectionModel().getSelectedItem();
@Override
protected String call() {
int ms = new Random().nextInt(10000);
println(String.format("Will return %s in %.2fs", selectedItem, 0.001 * ms));
// Do long-running task (takes random time)
long limitMillis = System.currentTimeMillis() + ms;
while (System.currentTimeMillis() < limitMillis) {
}
println("Returned " + selectedItem);
return "You have selected item: " + selectedItem;
}
}
}