JavaFX定期后台任务

Mic*_*zek 60 javafx-2

我尝试定期在JavaFX应用程序后台线程中运行,这会修改一些GUI属性.

我想我知道如何使用TaskService类,javafx.concurrent并且无法弄清楚如何在不使用Thread#sleep()方法的情况下运行这样的周期性任务.如果我可以使用一些Executor来自Executors制造方法(Executors.newSingleThreadScheduledExecutor())会很好

我试图Runnable每5秒运行一次,重启javafx.concurrent.Service但它会立即挂起,service.restart甚至service.getState()被调用.

所以最后我使用Executors.newSingleThreadScheduledExecutor(),它Runnable每隔5秒发射一次并使用以下命令Runnable运行另一个Runnable:

Platform.runLater(new Runnable() {
 //here i can modify GUI properties
}
Run Code Online (Sandbox Code Playgroud)

它看起来非常讨厌:(有没有更好的方法来使用TaskService类?

Ser*_*nev 98

您可以使用时间轴来解决问题:

Timeline fiveSecondsWonder = new Timeline(new KeyFrame(Duration.seconds(5), new EventHandler<ActionEvent>() {

    @Override
    public void handle(ActionEvent event) {
        System.out.println("this is called every 5 seconds on UI thread");
    }
}));
fiveSecondsWonder.setCycleCount(Timeline.INDEFINITE);
fiveSecondsWonder.play();
Run Code Online (Sandbox Code Playgroud)

对于后台进程(对UI没有任何作用),您可以使用旧商品java.util.Timer:

new Timer().schedule(
    new TimerTask() {

        @Override
        public void run() {
            System.out.println("ping");
        }
    }, 0, 5000);
Run Code Online (Sandbox Code Playgroud)

  • @KshitizSharma这是JavaFX UI应用程序的代码,你不能只从main()方法运行它.请参阅此处的完整示例:http://pastebin.com/tyLKxmB6 (3认同)
  • 你也可能会发现有用的`javafx.animation.AnimationTimer` (2认同)
  • 请注意,只要 Timer 线程不会尝试修改某些 javafx 元素,就可以,否则它将引发异常...:o (2认同)

Sla*_*law 25

前言:这个问题通常是询问如何在 JavaFX 中执行周期性操作的问题的重复目标,该操作是否应该在后台完成。虽然这个问题已经有了很好的答案,但这个答案试图将所有给定的信息(以及更多)整合到一个答案中,并解释/显示每种方法之间的差异。

此答案侧重于 JavaSE 和 JavaFX 中可用的 API,而不是第三方库,例如 ReactFX(在Tomas Mikula 的答案中展示)。


背景信息:JavaFX 和线程

与大多数主流 GUI 框架一样,JavaFX 是单线程的。这意味着有一个线程专用于读取和写入 UI 的状态以及处理用户生成的事件(例如鼠标事件、按键事件等)。在 JavaFX 中,此线程称为“JavaFX 应用程序线程”,有时简称为“FX 线程”,但其他框架可能会称其为其他名称。其他一些名称包括“UI 线程”、“事件调度线程”和“主线程”。

绝对重要的是,任何连接到屏幕上显示的 GUI 的东西都只能在JavaFX 应用程序线程上访问或操作。JavaFX 框架不是线程安全的,使用不同的线程不正确地读取或写入 UI 的状态会导致未定义的行为。即使您没有看到任何外部可见的问题,也可以在没有必要同步的情况下访问线程之间共享的状态也是损坏的代码。

然而,许多 GUI 对象可以在任何线程上操作,只要它们不是“活动的”。从文档javafx.scene.Node

节点对象可以被构造和修改上的任何线程,只要它们还没有附着到Scene在一个Windowshowing [加上强调]。应用程序必须将节点附加到这样的场景或在 JavaFX 应用程序线程上修改它们。

但是其他 GUI 对象,例如Window甚至Node(eg WebView) 的某些子类,则更加严格。例如,从文档中javafx.stage.Window

必须在 JavaFX 应用程序线程上构造和修改窗口对象。

如果您不确定 GUI 对象的线程规则,则其文档应提供所需的信息。

由于 JavaFX 是单线程的,您还必须确保永远不会阻塞或以其他方式独占 FX 线程。如果线程不能自由地完成其工作,则永远不会重绘 UI,并且无法处理新的用户生成的事件。不遵守此规则可能会导致臭名昭著的无响应/冻结 UI,并且您的用户不满意。

使JavaFX Application Thread休眠几乎总是错误的。


周期性任务

至少就本答案而言,有两种不同类型的周期性任务:

  1. 周期性前景“任务”。
    • 这可能包括诸如“闪烁”节点或定期在图像之间切换之类的东西。
  2. 周期性背景任务。
    • 一个例子可能是定期检查远程服务器的更新,如果有更新,下载新信息并将其显示给用户。

周期性前台任务

如果您的周期性任务既短又简单,那么使用后台线程就有点过分了,只会增加不必要的复杂性。更合适的解决方案是使用javafx.animationAPI。动画是异步的,但完全保留在JavaFX 应用程序线程中。换句话说,动画提供了一种在 FX 线程上“循环”的方法,在每次迭代之间有延迟,而无需实际使用循环。

有三个类特别适合周期性的前台任务。

时间线

ATimeline由一个或多个KeyFrames 组成。每个KeyFrame都有指定的完成时间。每个人还可以有一个“完成时”处理程序,在指定的时间段过去后调用该处理程序。这意味着您可以Timeline使用单个创建一个KeyFrame周期性地执行一个动作,根据需要循环多次(包括永远)。

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class App extends Application {

  @Override
  public void start(Stage primaryStage) {
    Rectangle rect = new Rectangle(100, 100);

    // toggle the visibility of 'rect' every 500ms
    Timeline timeline =
        new Timeline(new KeyFrame(Duration.millis(500), e -> rect.setVisible(!rect.isVisible())));
    timeline.setCycleCount(Animation.INDEFINITE); // loop forever
    timeline.play();

    primaryStage.setScene(new Scene(new StackPane(rect), 200, 200));
    primaryStage.show();
  }
}
Run Code Online (Sandbox Code Playgroud)

由于 aTimeline可以有多个,KeyFrame因此可以在不同的时间间隔执行操作。请记住,每个时间KeyFrame 不叠加。如果您有一个KeyFrame时间为两秒的另一个KeyFrame时间为两秒的时间,则两个KeyFrames 都将在动画开始后两秒完成。要KeyFrame在第一个之后两秒完成第二个,其时间需要为秒。

暂停过渡

与其他动画类不同, aPauseTransition不用于实际制作任何动画。它的主要目的是用作SequentialTransition在其他两个动画之间暂停的孩子。然而,就像Animation它的所有子类一样,它可以有一个在完成后执行的“完成时”处理程序,允许它用于周期性任务。

import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class App extends Application {

  @Override
  public void start(Stage primaryStage) {
    Rectangle rect = new Rectangle(100, 100);

    // toggle the visibility of 'rect' every 500ms
    PauseTransition pause = new PauseTransition(Duration.millis(500));
    pause.setOnFinished(
        e -> {
          rect.setVisible(!rect.isVisible());
          pause.playFromStart(); // loop again
        });
    pause.play();

    primaryStage.setScene(new Scene(new StackPane(rect), 200, 200));
    primaryStage.show();
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意完成时的处理程序调用playFromStart(). 这是再次“循环”动画所必需的。cycleCount无法使用该属性,因为在每个循环结束时不会调用 on-finished 处理程序,它仅在最后一个循环结束时调用。同样的事情是真实的Timeline; 它与Timeline上面一起工作的原因是因为完成的处理程序没有注册TimelineKeyFrame.

由于该cycleCount属性不能PauseTransition用于多个循环,因此仅循环一定次数(而不是永远)变得更加困难。您必须自己跟踪状态,并且仅playFromStart()在适当的时候调用。请记住,局部变量在 lambda 表达式或匿名类之外声明但在所述 lambda 表达式或匿名类内部使用的必须是最终的或有效的最终。

动画计时器

AnimationTimer班是JavaFX的动画API的最低水平。它不是 的子类,Animation因此没有上面使用的任何属性。取而代之的是,它有,当定时器被启动时,在当前帧的调用一次每帧与该时戳(以纳秒为单位)的抽象方法:#handle(long)。为了定期执行某些操作AnimationTimer(除了每帧一次)将需要手动计算handle使用方法参数的调用之间的时间差。

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class App extends Application {

  @Override
  public void start(Stage primaryStage) {
    Rectangle rect = new Rectangle(100, 100);

    // toggle the visibility of 'rect' every 500ms
    AnimationTimer timer =
        new AnimationTimer() {

          private long lastToggle;

          @Override
          public void handle(long now) {
            if (lastToggle == 0L) {
              lastToggle = now;
            } else {
              long diff = now - lastToggle;
              if (diff >= 500_000_000L) { // 500,000,000ns == 500ms
                rect.setVisible(!rect.isVisible());
                lastToggle = now;
              }
            }
          }
        };
    timer.start();

    primaryStage.setScene(new Scene(new StackPane(rect), 200, 200));
    primaryStage.show();
  }
}
Run Code Online (Sandbox Code Playgroud)

对于与上述类似的大多数用例,使用TimelinePauseTransition将是更好的选择。

周期性后台任务

如果您的周期性任务很耗时(例如昂贵的计算)或阻塞(例如 I/O),则需要使用后台线程。JavaFX 附带了一些内置的并发实用程序,以帮助后台线程和 FX 线程之间进行通信。这些实用程序的描述如下:

对于需要与 FX 线程通信的周期性后台任务,要使用的类是javafx.concurrent.ScheduledService. 该类将定期执行其任务,成功执行后重新启动,基于指定的时间段。如果配置为这样做,它甚至会在执行失败后重试可配置的次数。

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.concurrent.Worker.State;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class App extends Application {

  // maintain a strong reference to the service
  private UpdateCheckService service;

  @Override
  public void start(Stage primaryStage) {
    service = new UpdateCheckService();
    service.setPeriod(Duration.seconds(5));

    Label resultLabel = new Label();
    service.setOnRunning(e -> resultLabel.setText(null));
    service.setOnSucceeded(
        e -> {
          if (service.getValue()) {
            resultLabel.setText("UPDATES AVAILABLE");
          } else {
            resultLabel.setText("UP-TO-DATE");
          }
        });

    Label msgLabel = new Label();
    msgLabel.textProperty().bind(service.messageProperty());

    ProgressBar progBar = new ProgressBar();
    progBar.setMaxWidth(Double.MAX_VALUE);
    progBar.progressProperty().bind(service.progressProperty());
    progBar.visibleProperty().bind(service.stateProperty().isEqualTo(State.RUNNING));

    VBox box = new VBox(3, msgLabel, progBar);
    box.setMaxHeight(Region.USE_PREF_SIZE);
    box.setPadding(new Insets(3));

    StackPane root = new StackPane(resultLabel, box);
    StackPane.setAlignment(box, Pos.BOTTOM_LEFT);

    primaryStage.setScene(new Scene(root, 400, 200));
    primaryStage.show();

    service.start();
  }

  private static class UpdateCheckService extends ScheduledService<Boolean> {

    @Override
    protected Task<Boolean> createTask() {
      return new Task<>() {

        @Override
        protected Boolean call() throws Exception {
          updateMessage("Checking for updates...");
          for (int i = 0; i < 1000; i++) {
            updateProgress(i + 1, 1000);
            Thread.sleep(1L); // fake time-consuming work
          }
          return Math.random() < 0.5; // 50-50 chance updates are "available"
        }
      };
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

这是文档中的注释ScheduledService

这门课的时间不是绝对可靠的。一个非常繁忙的事件线程可能会在后台任务的执行开始时引入一些时间延迟,因此周期或延迟的非常小的值可能不准确。数百毫秒或更长的延迟或周期应该是相当可靠的。

还有一个:

ScheduledService引入了一个名为新的属性lastValue。的lastValue是,在去年成功地计算出的值。因为 a在每次运行时Service清除其value属性,并且因为ScheduledService将在完成后立即重新安排运行(除非它进入取消或失败状态),所以该value属性在ScheduledService. 在大多数情况下,您会希望改用lastValue.

最后一个注释意味着绑定到 a 的value属性ScheduledService很可能是无用的。尽管查询了value属性,但上面的示例仍然有效,因为在onSucceeded重新安排服务之前在处理程序中查询了该属性。

不与 UI 交互

如果周期性后台任务不需要与 UI 交互,那么您可以改用 Java 的标准 API。更具体地说,要么:

注意ScheduledExecutorService支持线程,不像Timer只支持单线程。

ScheduledService 不是一个选项

如果由于某种原因您不能使用ScheduledService,但无论如何都需要与 UI 交互,那么您需要确保与 UI 交互的代码,并且只有该代码在 FX 线程上执行。这可以通过使用来完成Platform#runLater(Runnable)

在未来某个未指定的时间在 JavaFX 应用程序线程上运行指定的 Runnable。这个方法可以从任何线程调用,它将 Runnable 发布到一个事件队列,然后立即返回给调用者。Runnables 按照它们发布的顺序执行。传入 runLater 方法的 runnable 将在任何传入 runLater 的 Runnable 之前执行。如果在 JavaFX 运行时关闭后调用此方法,则该调用将被忽略:不会执行 Runnable 并且不会抛出异常。

注意:应用程序应该避免用太多挂起的 Runnables 淹没 JavaFX。否则,应用程序可能会变得无响应。鼓励应用程序将多个操作批处理为更少的 runLater 调用。此外,应尽可能在后台线程上完成长时间运行的操作,从而为 GUI 操作释放 JavaFX 应用程序线程。

[...]

请注意上述文档中的注释。该javafx.concurent.Task班由合并更新其避免了这个messageprogressvalue性能。这目前是通过使用AtomicReference策略性的 get-and-set 操作来实现的。如果有兴趣,可以看一下实现(JavaFX 是开源的)。


Mar*_*cel 12

我更喜欢PauseTransition:

    PauseTransition wait = new PauseTransition(Duration.seconds(5));
    wait.setOnFinished((e) -> {
        /*YOUR METHOD*/
        wait.playFromStart();
    });
    wait.play();
Run Code Online (Sandbox Code Playgroud)


Tom*_*ula 7

这是使用Java 8和ReactFX的解决方案.假设您想要定期重新计算其值Label.textProperty().

Label label = ...;

EventStreams.ticks(Duration.ofSeconds(5))          // emits periodic ticks
    .supplyCompletionStage(() -> getStatusAsync()) // starts a background task on each tick
    .await()                                       // emits task results, when ready
    .subscribe(label::setText);                    // performs label.setText() for each result

CompletionStage<String> getStatusAsync() {
    return CompletableFuture.supplyAsync(() -> getStatusFromNetwork());
}

String getStatusFromNetwork() {
    // ...
}
Run Code Online (Sandbox Code Playgroud)

与Sergey的解决方案相比,您不会将整个线程专用于从网络获取状态,而是使用共享线程池.


Ren*_*mes 5

ScheduledService你也可以使用。我注意到在使用过程中,我的应用程序中发生了一些 UI 冻结,特别是当用户与 a 的元素交互(在 JavaFX 12 上)时,我正在使用此Timeline替代PauseTransition方案MenuBar。使用后ScheduledService不再出现这些问题。

class UpdateLabel extends ScheduledService<Void> {

   private Label label;

   public UpdateLabel(Label label){
      this.label = label;
   }

   @Override
   protected Task<Void> createTask(){
      return new Task<Void>(){
         @Override
         protected Void call(){
           Platform.runLater(() -> {
              /* Modify you GUI properties... */
              label.setText(new Random().toString());
           });
           return null;
         }
      }
   }
}
Run Code Online (Sandbox Code Playgroud)

然后,使用它:

class WindowController implements Initializable {

   private @FXML Label randomNumber;

   @Override
   public void initialize(URL u, ResourceBundle res){
      var service = new UpdateLabel(randomNumber);
      service.setPeriod(Duration.seconds(2)); // The interval between executions.
      service.play()
   }
}
Run Code Online (Sandbox Code Playgroud)