任务执行前调用的 Task#call() 方法

Task#call() method invoked before task is executed

根据文档,Task#call() 是 "invoked when the Task is executed "。 考虑以下程序:

import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.stage.Stage;

public class TestTask extends Application {

    Long start;

    public void start(Stage stage) {

        start = System.currentTimeMillis();

        new Thread(new Taskus()).start(); 
    }

    public static void main(String[] args) {
        launch();
    }

    class Taskus extends Task<Void> {

        public Taskus() {
            stateProperty().addListener((obs, oldValue, newValue) -> {
                try {
                    System.out.println(newValue + " at " + (System.currentTimeMillis()-start));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

        public Void call() throws InterruptedException {

            for (int i = 0; i < 10000; i++) {
                // Could be a lot longer.
            }
            System.out.println("Some code already executed." + " at " + (System.currentTimeMillis()-start));

            Thread.sleep(3000);

            return null;
        }
    }
}

执行这个程序得到以下输出:

Some code already executed. after 5 milliseconds
SCHEDULED after 5 milliseconds
RUNNING after 7 milliseconds
SUCCEEDED after 3005 milliseconds

为什么 call() 方法甚至在任务计划之前就被调用了?这对我来说毫无意义。在我第一次看到问题的任务中,我的任务在任务进入 SCHEDULED 状态前几秒钟执行。如果我想给用户一些关于状态的反馈,但直到任务已经执行了几秒钟才发生任何事情怎么办?

Why is the call() method invoked before the task is even scheduled?

TLDR;版本:不是。它只是在您收到已安排的通知之前调用。


您有两个线程 运行ning,基本上是独立的:您显式创建的线程和 FX 应用程序线程。当您启动应用程序线程时,它将在该线程上调用 Taskus.call()。但是,通过调用 Platform.runLater(...).

在 FX 应用程序线程上更改任务的属性

因此,当您在线程上调用 start() 时,会在幕后发生以下情况:

  1. 新线程已启动
  2. 在该线程上,调用了 Task 中的内部 call() 方法。该方法:
  3. 安排一个 运行 能够在 FX 应用程序线程上执行,将任务的 stateProperty 更改为 SCHEDULED
  4. 安排一个 运行 能够在 FX 应用程序线程上执行,将任务的 stateProperty 更改为 RUNNING
  5. 调用您的 call 方法

当 FX 应用程序线程收到 运行nable 时,将任务状态从 READY 更改为 SCHEDULED,然后从 SCHEDULED 更改为 [=21] =],它会影响这些更改并通知所有听众。由于这与 call 方法中的代码在不同的线程上,因此 call 方法中的代码与 stateProperty 侦听器中的代码之间没有 "happens-before" 关系。换句话说,无法保证哪个会先发生。特别是,如果 FX 应用程序线程已经忙于做某事(呈现 UI、处理用户输入、处理传递给 Platform.runLater(...) 的其他 Runnables 等),它将完成之前的那些它对任务的 stateProperty 进行更改。

您可以保证对 SCHEDULEDRUNNING 的更改将 计划 在 FX 应用程序线程上(但不一定执行)在调用 call 方法之前,对 SCHEDULED 的更改将在对 RUNNING 的更改执行之前执行。

打个比方。假设我接受客户的请求来编写软件。将我的工作流程视为后台线程。假设我有一个行政助理为我与客户沟通。将她的工作流程想象成 FX 应用程序线程。因此,当我收到客户的请求时,我告诉我的行政助理给客户发电子邮件并通知他们我收到了请求 (SCHEDULED)。我的行政助理尽职尽责地把它放在她的 "to-do" 列表中。不久之后,我告诉我的行政助理给客户发电子邮件,告诉他们我已经开始处理他们的项目 (RUNNING),然后她将其添加到她的 "to-do" 列表中。然后我开始做这个项目。我在该项目上做了一些工作,然后转到 Twitter 并 post 一条推文(您的 System.out.println("Some code already executed"))"Working on a project for xxx, it's really interesting!"。根据我助理 "to-do" 列表中已有的东西的数量,推文很可能会在她向客户发送电子邮件之前出现,因此客户很可能在看到我之前就已经开始了该项目的工作电子邮件说工作已安排,即使从我的工作流程的角度来看,一切都按正确的顺序进行。

这通常是您想要的:状态 属性 旨在用于更新 UI,因此它必须 运行 在 FX 应用程序线程上。由于您 运行 在不同的线程上执行任务,您可能希望它这样做:运行 在不同的执行线程中。

在调用方法实际开始执行后的很长一段时间(超过一个帧渲染脉冲,通常为 1/60 秒),我似乎不太可能观察到对预定状态的更改:如果这是发生这种情况,您可能会在某处阻塞 FX Application 线程以防止它看到这些更改。在您的示例中,时间延迟显然是最小的(小于一毫秒)。

如果您想在任务开始时做某事,但不关心在哪个线程上做,只需在调用方法的开头做。 (按照上面的类比,这相当于我把邮件发给客户,而不是让我的助理去做。)

如果您确实需要在 FX 应用程序线程上发生某些用户通知后调用方法中的代码,您需要使用以下模式:

public class Taskus extends Task<Void> {

    @Override
    public Void call() throws Exception {
        FutureTask<Void> uiUpdate = new FutureTask<Void>(() -> {
            System.out.println("Task has started");
            // do some UI update here...
            return null ;
        });
        Platform.runLater(uiUpdate);
        // wait for update:
        uiUpdate.get();
        for (int i = 0; i < 10000; i++) {
            // any VM implementation worth using is going 
            // to ignore this loop, by the way...
        }
        System.out.println("Some code already executed." + " at " + (System.currentTimeMillis()-start));
        Thread.sleep(3000);
        return null ;
    }
}

在此示例中,保证您在看到 "Some code already executed" 之前看到 "Task has started"。此外,由于显示 "Task has started" 方法发生在与状态更改为 SCHEDULEDRUNNING 相同的线程(FX 应用程序线程)上,并且由于显示 "Task has started" 消息安排在这些状态更改之后,您一定会在看到 "Task has started" 消息之前看到到 SCHEDULEDRUNNING 的转换。 (打个比方,这就像我让我的助理发邮件,然后我知道她已经发了才开始工作。)

另请注意,如果您将原来的调用替换为

System.out.println("Some code already executed." + " at " + (System.currentTimeMillis()-start));

Platform.runLater(() -> 
    System.out.println("Some code already executed." + " at " + (System.currentTimeMillis()-start)));

那么您也可以保证按您期望的顺序看到呼叫:

SCHEDULED after 5 milliseconds
RUNNING after 7 milliseconds
Some code already executed. after 8 milliseconds
SUCCEEDED after 3008 milliseconds

最后一个版本相当于我让我的助手为我 post 推文的类比。