如果启动了新的任务实例,如何取消任务?

Zep*_*hyr 5 java java.util.concurrent

我的应用程序包含一个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();

    }
}
Run Code Online (Sandbox Code Playgroud)

当以随机顺序点击几个项目时,结果是不可预测的.我需要的是更新标签以显示最后Task执行的结果.

我是否需要以某种方式安排任务或将其添加到服务中以取消所有先前的任务?

编辑:

在我的真实世界应用程序中,用户从中选择一个项目,ListView后台任务读取数据库(复杂SELECT语句)以获取与该项目相关的所有信息.然后,这些细节将显示在应用程序中.

发生的问题是当用户选择项目但更改其选择时,应用程序中显示的返回数据可能是针对所选的第一个项目,即使现在选择了完全不同的项目.

从第一个(即:不需要的)选择返回的任何数据都可以完全丢弃.

Sla*_*law 4

正如这个答案所提到的,您的要求似乎是使用Service. A允许您在任何给定时间以可重用1 的Service方式运行一个 。当您取消, via时,它会取消底层. 它还会为您跟踪自己的信息,因此您无需将它们保存在列表中的某个位置。TaskServiceService.cancel()TaskServiceTask

使用 MVCE,您想要做的是创建一个Service包装您的Task. 每次用户在 中选择一个新项目时,ListView您都会取消Service,更新必要的状态,然后重新启动Service。然后,您可以使用Service.setOnSucceeded回调将结果设置为Label. 这保证了只有最后一次成功的执行才会返回给您。即使之前取消的Task仍然返回结果,Service也会忽略它们。

您也无需担心外部同步(至少在您的 MVCE 中)。所有涉及启动、取消和观察Service发生在 FX 线程上的操作。唯一不在FX 线程上执行的代码(如下所示)将在内部Task.call()(以及当类实例化时立即分配的字段,我相信这发生在 JavaFX-Launcher 线程上)。

这是 MVCE 的修改版本,使用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.");
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

当您调用时,Service.start()它会创建 a并使用其executor propertyTask中包含的current 执行它。如果该属性包含,则它使用一些未指定的默认值(使用守护线程)。ExecutornullExecutor

在上面,您可以看到我reset()在取消后以及回调中onSucceeded进行了调用onFailed。这是因为 aService只能在处于READY 状态时启动。您可以使用restart()而不是start()如果需要的话。它基本上相当于调用cancel()-> reset()-> start()

1不可Task重复使用。相反,每次启动时都会Service创建一个新的。Task


当您取消一个时,Service它会取消当前正在运行的Task,如果有的话。即使ServiceTask被取消,并不意味着执行实际上已停止。在Java中,取消后台任务需要与该任务的开发人员合作。

这种合作采取定期检查执行是否应该停止的形式。如果使用正常RunnableCallable这将需要检查当前的中断状态Thread或使用某些boolean标志2。由于Task扩展,FutureTask您还可以使用isCancelled()Future接口继承的方法。如果您由于某种原因无法使用isCancelled()(称为外部代码,不使用 aTask等),那么您可以使用以下命令检查线程中断:

您可以通过 获取对当前线程的引用Thread.currentThread()

在后台代码中,您需要在适当的位置检查当前线程是否已被中断、标志是否boolean已设置或是否Task已被取消。如果有,那么您将执行任何必要的清理并停止执行(通过返回或引发异常)。

另外,如果您Thread正在等待某些可中断的操作,例如阻塞 IO,那么它会InterruptedException在中断时抛出异常。在你的 MVCE 中你使用的Thread.sleep是可中断的;这意味着当您调用取消时,此方法将抛出提到的异常。

当我上面说“清理”时,我的意思是在后台进行任何必要的清理,因为您仍在后台线程上。如果您需要清理 FX 线程上的任何内容(例如更新 UI),那么您可以使用onCancelled的属性Service

在上面的代码中,您还会看到我使用了受保护的方法succeeded()cancelled(). 和TaskService提供这些方法(以及各种其他方法Worker.State),并且它们将始终在 FX 线程上调用。但是,请阅读文档,因为ScheduledService需要您调用其中一些方法的超级实现。

2如果使用boolean标志,请确保对其进行的更新对其他线程可见。您可以通过制作volatile、同步或使用java.util.concurrent.atomic.AtomicBoolean.