当前位置: 首页 > 知识库问答 >
问题:

JavaFX周期性后台任务

帅博远
2023-03-14

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

我想我知道如何使用任务服务类从javafx.concurrent和不能弄清楚如何运行这样的周期性任务不使用线程#睡眠()方法。这将是很好的,如果我可以使用一些执行器执行器编造方法(Executors.newSingleThreadschduledExecator()

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

最后我使用执行器。newSingleThreadScheduledExecutor(),它每5秒启动一次myRunnable,并使用以下命令运行另一个Runnable

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

这看起来很糟糕:(有没有更好的方法使用任务服务类来实现这一点?

共有3个答案

冀崇凛
2023-03-14

我更喜欢暂停过渡:

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

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

这个答案主要关注JavaSE和JavaFX中可用的API,而不是第三方库,如ReactFX(如Tomas Mikula的答案所示)。

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

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

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

节点对象可以在任何线程上构造和修改,只要它们尚未连接到窗口中的场景,即显示[emphasis added]。应用程序必须将节点附加到这样的场景,或在JavaFX应用程序线程上修改它们。

但是其他GUI对象,如Windows,甚至Node的一些子类(例如WebView),则更加严格。例如,从javafx.stage.Windows的留档:

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

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

由于JavaFX是单线程的,所以您还必须确保永远不要阻塞或垄断FX线程。如果线程不能自由完成其工作,那么用户界面永远不会被重新绘制,新的用户生成的事件也无法处理。不遵守这条规则可能会导致臭名昭著的无响应/冻结用户界面,用户也会不高兴。

实际上,HibernateJavaFX应用程序线程总是错误的。

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

  • 定期前台“任务”。
    • 这可能包括“闪烁”节点或定期在图像之间切换
    • 例如,定期检查远程服务器是否有更新,如果有,下载新信息并显示给用户

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

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

    一个Timeline由一个或多个KeyFrame组成。每个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();
      }
    }
    

    由于时间线可以有多个关键帧,因此可以在不同的时间间隔执行操作。请记住,每个关键帧的时间不会叠加。如果一个关键帧的时间为两秒,然后是另一个关键帧的时间为两秒,则两个关键帧都将在动画启动后两秒完成。要使第二个关键帧在第一个关键帧之后两秒完成,其时间需要为四秒。

    与其他动画类不同,PauseTransition不用于实际制作任何动画。它的主要用途是作为SequentialTransition的子对象,在其他两个动画之间暂停。然而,与动画的所有子类一样,它可以有一个“on finished”处理程序,在完成后执行,允许用于定期任务。

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

    注意,on finished处理程序调用playFromStart()。这是再次“循环”动画所必需的。无法使用cycleCount属性,因为在每个周期结束时不会调用已完成的处理程序,它只在上一个周期结束时调用。同样的事情也适用于时间线;它与上面的时间轴一起工作的原因是,已完成的处理程序没有注册到时间轴,而是注册到关键帧

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

    类是JavaFX动画API的最低级别。它不是Animation的子类,因此没有上面使用的任何属性。相反,它有一个抽象方法,当计时器启动时,每帧调用一次,时间戳为当前帧的时间戳(以纳秒为单位):#句柄(long)。为了使用AnimationTimer(每帧一次除外)周期性地执行某件事情,需要使用方法的参数手动计算句柄调用之间的时间差。

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

    对于大多数与上述类似的用例,使用时间线暂停转换都是更好的选择。

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

    • JavaFX教程中的并发性,以及
    • javafx中类的文档。并发

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

    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文档中的注释:

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

    还有一个:

    #######################################################################################################################################################lastValue是上次成功计算的值。因为Service在每次运行时都会清除其value属性,并且因为schduledService将在运行完成后立即重新安排运行(除非它进入已取消或失败状态),所以value属性不是在SchduledService上非常有用。在大多数情况下,您将希望使用lastValue返回的值。

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

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

    • java。util。Timerclass(而不是javax.swing.Timer),
    • 或者更现代的java。util。同时发生的ScheduledExecutorService接口

    请注意,ScheduledExecutorService支持线程池,而Timer只支持单个线程。

    如果出于任何原因,您不能使用ScheduledService,但仍然需要与UI交互,那么您需要确保与UI交互的代码,并且只有该代码在FX线程上执行。这可以通过使用平台#runLater(Runnable)来实现。

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

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

    [...]

    注意上面的留档。javafx.concurent.任务类通过合并对其消息进度属性的更新来避免这种情况。这目前是通过使用原子引用和策略获取和设置操作来实现的。如果有兴趣,可以看看实现(JavaFX是开源的)。

齐飞星
2023-03-14

您可以使用该任务的时间线:

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做任何事情),您可以使用旧的goodjava.util.Timer

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

        @Override
        public void run() {
            System.out.println("ping");
        }
    }, 0, 5000);
 类似资料:
  • 问题内容: 我尝试定期在JavaFX应用程序后台线程中运行,这会修改一些GUI属性。 我想我知道如何使用Task和Service类,并且不知道如何在不使用Thread#sleep()方法的情况下运行此类定期任务。这将是很好,如果我可以使用一些Executor从Executors编造的方法) 我尝试Runnable每5秒运行一次,这会重新启动,javafx.concurrent.Service但立即

  • 我很难找到有关 Xamarin.Forms 的后台任务支持的文档。Xamarin.Forms 是否为定期后台任务提供支持? 我需要为Windows手机10和Android实现它。

  • 执行周期性任务也是一样简单,您只需要编写一行代码: RecurringJob.AddOrUpdate(() => Console.Write("Easy!"), Cron.Daily); 此行在持久存储中创建一个新实体。 Hangfire Server中的一个特殊组件(请参阅 处理后台任务) 以分钟为间隔检查周期性任务,然后在队列中将其视作 fire-and-forget 任务。这样就可以照常跟踪

  • 我正在开发一个登录网站的程序。它获得一些值,然后每135分钟左右在网站上进行一定的点击。值,“obtained_value”是从网站上读取的,每次点击程序都会递减一些值。我想运行程序,直到获得的值小于10。一旦发生这种情况,我想暂停程序直到达到目标时间并重新启动点击循环。我想在每次达到目标时间时都这样做。我在以下代码中实现了这个逻辑,但我的代码在达到目标时间后保持睡眠,而不是重新启动循环。我如何解

  • 可以通过设置任务详情页中的周期,实现建立周期性任务。比如“周例会”

  • 说明 在后台任务中,因为是运行在task进程,所以无法使用协程和协程客户端。如果你要操作数据库或Redis,一定要配置同步的连接池。 配置 项目配置文件中mainServer.configs.task_worker_num一定要配置为大于0,否则无法使用后台任务。 定义任务执行类 <?php namespace Test; use Imi\Task\TaskParam; use Imi\Task