JavaFX定期后台任务


68

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

我想我知道如何使用TaskService类,javafx.concurrent并且无法弄清楚如何在不使用Thread#sleep()方法的情况下运行此类定期任务。这将是很好,如果我可以使用一些ExecutorExecutors编造的方法(Executors.newSingleThreadScheduledExecutor()

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

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

Platform.runLater(new Runnable() {
 //here i can modify GUI properties
}

看起来很讨厌:(是否有更好的方法使用TaskService类来执行此操作?

Answers:


116

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

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();

对于后台进程(对UI不起任何作用),您可以使用old good java.util.Timer

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

        @Override
        public void run() {
            System.out.println("ping");
        }
    }, 0, 5000);

1
对。对于辅助线程,最好使用old java.util.Timer。我用示例更新了答案。
Sergey Grinev'4

2
您可能还会发现有用javafx.animation.AnimationTimer
Sergey Grinev 2013年

1
@SergeyGrinev您的第一个示例不起作用。当我运行它时,屏幕上没有输出,程序仅终止。
2013年

3
@KshitizSharma这是JavaFX UI应用程序的代码,您不能只从main()方法运行它。在此处查看完整示例:pastebin.com/tyLKxmB6
Sergey Grinev 2013年

1
请注意,只要不尝试修改某些javafx元素,就可以使用Timer线程,否则,它将引发Exception ...:o
Hugo Zaragoza

17

前言:该问题通常是重复问题的目标,这些问题询问如何在JavaFX中执行定期操作,无论该操作是否应在后台执行。尽管该问题已经有了不错的答案,但该答案试图将所有给定的信息(以及更多信息)合并为一个答案,并说明/显示每种方法之间的差异。

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


背景信息:JavaFX和线程

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

绝对重要的是,只有在JavaFX Application Thread上才能访问或操纵与屏幕上显示的GUI相连的任何内容。JavaFX框架不是线程安全的,使用其他线程来不正确地读取或写入UI状态可能会导致未定义的行为。即使您看不到任何外部可见的问题,在没有必要的同步的情况下访问线程之间共享的状态也会破坏代码。

但是,许多GUI对象只要不处于活动状态就可以在任何线程上进行操作。从以下文档中javafx.scene.Node

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

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

窗口对象必须在JavaFX Application Thread上构造和修改。

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

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

它几乎总是错误的睡觉JavaFX应用程序线程


定期任务

有两种不同类型的定期任务,至少出于此答案的目的:

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

定期前台任务

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

有三类独特地适合于周期性前台任务。

时间线

ATimeline由一个或多个KeyFrames组成。每个KeyFrame都有一个指定的完成时间。每个人也可以有一个“完成”处理程序,该处理程序在指定的时间量过去之后被调用。这意味着您可以创建一个Timeline具有单个的KeyFrame,该定期执行一个动作,并根据需要进行多次循环(包括ever)。

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();
  }
}

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

暂停过渡

与其他动画类不同,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();
  }
}

注意,完成的处理程序将调用playFromStart()。这是再次“循环”动画的必要条件。该cycleCount属性不能使用,因为未完成的处理程序不会在每个周期的末尾调用,而是仅在最后一个周期的末尾调用。同样的事实也是如此Timeline; 之所以可以在Timeline上面使用它,是因为未完成的处理程序未在中注册,Timeline而是在中注册了KeyFrame

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

动画计时器

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();
  }
}

对于与上述相似的大多数用例,使用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"
        }
      };
    }
  }
}

这是来自以下文档的注释ScheduledService

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

还有一个:

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

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

与用户界面无交互

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

请注意,它ScheduledExecutorService支持线程,而Timer后者仅支持单个线程。

ScheduledService不是一个选项

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

将来在某些未指定的时间在JavaFX Application Thread上运行指定的Runnable。可以从任何线程调用此方法,该方法会将Runnable张贴到事件队列中,然后立即返回给调用者。Runnable按照其发布的顺序执行。传递给runLater方法的runnable将在传递给后续对runLater的调用的任何Runnable之前执行。如果在关闭JavaFX运行时之后调用此方法,则该调用将被忽略:Runnable将不会执行,并且不会引发任何异常。

注意:应用程序应避免将太多未决Runnable泛洪到JavaFX。否则,应用程序可能无法响应。鼓励应用程序将多个操作分批到更少的runLater调用中。另外,应在可能的情况下在后台线程上执行长时间运行的操作,从而释放JavaFX Application Thread进行GUI操作。

[...]

注意上述文档中的注释。该javafx.concurent.Task班由合并更新其避免了这个messageprogressvalue性能。当前,这是通过使用AtomicReference战略性的获取和设置操作来实现的。如果有兴趣,您可以看一下实现(JavaFX是开源的)。


14

我更喜欢PauseTransition:

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

8

这是使用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() {
    // ...
}

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


3

您也可以使用ScheduledService。我注意到在使用Timeline和期间PauseTransition,我的应用程序中出现了一些UI冻结,尤其是当用户与MenuBar(在JavaFX 12上)与a的元素进行交互时,我使用了这种替代方法。使用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;
         }
      }
   }
}

然后,使用它:

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()
   }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.