JavaFX периодическая фоновая задача

Я пытаюсь периодически запускать в фоновом потоке приложения JavaFX, который изменяет некоторые свойства GUI.

Я думаю, что я знаю, как использовать Task а также Service классы от 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
}

Это выглядит очень противно:(Есть ли лучший способ сделать это, используя Task или же Service классы?

6 ответов

Решение

Вы можете использовать Timeline для чего бы то ни было:

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

для фоновых процессов (которые ничего не делают с пользовательским интерфейсом) вы можете использовать старый добрый java.util.Timer:

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

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

Предисловие: этот вопрос часто является повторяющейся целью для вопросов, которые спрашивают, как выполнять периодические действия в JavaFX, должно ли действие выполняться в фоновом режиме или нет. Хотя на этот вопрос уже есть отличные ответы, этот ответ пытается объединить всю предоставленную информацию (и многое другое) в один ответ и объяснить / показать различия между каждым подходом.

В этом ответе основное внимание уделяется API-интерфейсам, доступным в JavaSE и JavaFX, а не сторонним библиотекам, таким как ReactFX (продемонстрировано в ответе Томаса Микулы).


Справочная информация: JavaFX и потоки

Как и большинство основных сред GUI, JavaFX является однопоточным. Это означает, что существует один поток, предназначенный для чтения и записи состояния пользовательского интерфейса и обработки событий, генерируемых пользователем (например, событий мыши, ключевых событий и т. Д.). В JavaFX этот поток называется "потоком приложения JavaFX", иногда его сокращают до "потока FX", но в других фреймворках он может называться иначе. Некоторые другие названия включают "поток пользовательского интерфейса", "поток диспетчеризации событий" и "основной поток".

Абсолютно важно, чтобы все, что связано с графическим интерфейсом пользователя, отображаемым на экране, было доступно или управлялось только в потоке приложения JavaFX. Платформа JavaFX не является потокобезопасной, и использование другого потока для неправильного чтения или записи состояния пользовательского интерфейса может привести к неопределенному поведению. Даже если вы не видите никаких видимых извне проблем, доступ к общему состоянию между потоками без необходимой синхронизации является нарушенным кодом.

Однако многими объектами GUI можно манипулировать в любом потоке, если они не "живы". Из документации javafx.scene.Node:

Объекты узлов могут быть созданы и изменены в любом потоке, если они еще не прикреплены кScene в Window то есть showing [курсив добавлен]. Приложение должно присоединять узлы к такой сцене или изменять их в потоке приложения JavaFX.

Но другие объекты GUI, такие как Window и даже некоторые подклассы Node (например WebView), более строгие. Например, из документации javafx.stage.Window:

Оконные объекты должны создаваться и изменяться в потоке приложения JavaFX.

Если вы не уверены в правилах потоковой передачи объекта GUI, его документация должна предоставить необходимую информацию.

Поскольку JavaFX является однопоточным, вы также должны убедиться, что никогда не блокируете или иным образом монополизируете поток FX. Если поток не может выполнять свою работу, пользовательский интерфейс никогда не перерисовывается, и новые события, созданные пользователем, не могут быть обработаны. Несоблюдение этого правила может привести к печально известному зависанию / зависанию пользовательского интерфейса, и ваши пользователи будут недовольны.

Это практически всегда неправильно спать на тему JavaFX приложений.


Периодические задачи

Есть два разных типа периодических задач, по крайней мере, для целей этого ответа:

  1. Периодические "задачи" переднего плана.
    • Это может включать такие вещи, как "мигающий" узел или периодическое переключение между изображениями.
  2. Периодические фоновые задачи.
    • Примером может быть периодическая проверка удаленного сервера на наличие обновлений и, если таковые имеются, загрузка новой информации и отображение ее пользователю.

Периодические задачи переднего плана

Если ваша периодическая задача короткая и простая, то использование фонового потока излишне и просто добавляет ненужной сложности. Более подходящим решением является использование javafx.animation 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();
  }
}

Поскольку Timeline может иметь более одного KeyFrameвозможно, что действия будут выполняться с разными интервалами. Просто имейте в виду, что время каждогоKeyFrame не складывать. Если у тебя естьKeyFrame со временем две секунды, за которыми следует другой KeyFrame со временем в две секунды оба KeyFrames завершится через две секунды после начала анимации. Иметь второйKeyFrameзакончить через две секунды после первого, его время должно быть четыре секунды.

PauseTransition

В отличие от других классов анимации, PauseTransition не используется для реальной анимации. Его основная цель - быть ребенком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()при необходимости. Имейте в виду, что локальные переменные, объявленные вне лямбда-выражения или анонимного класса, но используемые внутри указанного лямбда-выражения или анонимного класса, должны быть окончательными или фактически окончательными.

AnimationTimer

В AnimationTimer class - это самый низкий уровень API анимации JavaFX. Это не подкласс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();
  }
}

Для большинства случаев использования, аналогичных описанным выше, можно использовать либо Timeline или PauseTransition был бы лучшим вариантом.

Периодические фоновые задачи

Если ваша периодическая задача требует много времени (например, дорогостоящие вычисления) или блокирует (например, ввод-вывод), то необходимо использовать фоновый поток. 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:

Сроки для этого класса не совсем надежны. Очень загруженный поток событий может привести к некоторой задержке по времени в начале выполнения фоновой задачи, поэтому очень маленькие значения для периода или задержки, вероятно, будут неточными. Задержка или период в сотни миллисекунд или более должны быть достаточно надежными.

И другой:

В ScheduledService вводит новое свойство под названием lastValue. ВlastValue- последнее успешно вычисленное значение. Потому чтоService очищает его value свойство при каждом запуске, и поскольку ScheduledService перепланирует запуск сразу после завершения (если только он не войдет в состояние отмены или ошибки), value свойство не слишком полезно на ScheduledService. В большинстве случаев вы захотите вместо этого использовать значение, возвращаемоеlastValue.

Последнее примечание означает привязку к value собственность ScheduledServiceпо всей вероятности бесполезен. Пример выше работает, несмотря на запросvalue свойство, потому что свойство запрашивается в onSucceeded обработчик, прежде чем обслуживание будет перенесено.

Нет взаимодействия с пользовательским интерфейсом

Если периодической фоновой задаче не требуется взаимодействовать с пользовательским интерфейсом, вы можете вместо этого использовать стандартные API-интерфейсы Java. В частности, либо:

Обратите внимание, что ScheduledExecutorServiceподдерживает пулы потоков, в отличие отTimer который поддерживает только один поток.

ScheduledService не является вариантом

Если по какой-либо причине вы не можете использовать ScheduledService, но вам все равно необходимо взаимодействовать с пользовательским интерфейсом, тогда вам нужно убедиться, что код, взаимодействующий с пользовательским интерфейсом, и только этот код, выполняется в потоке FX. Это можно сделать, используя Platform#runLater(Runnable).

Запустите указанный Runnable в потоке приложения JavaFX в неопределенное время в будущем. Этот метод, который может быть вызван из любого потока, отправит Runnable в очередь событий, а затем немедленно вернется к вызывающему. Runnables выполняются в порядке их публикации. Runnable, переданный в метод runLater, будет выполнен до того, как любой Runnable будет передан в последующий вызов runLater. Если этот метод вызывается после завершения работы среды выполнения JavaFX, вызов будет проигнорирован: Runnable не будет выполнен и исключение не будет создано.

ПРИМЕЧАНИЕ: приложениям следует избегать переполнения JavaFX слишком большим количеством ожидающих выполнения Runnables. В противном случае приложение может перестать отвечать. Приложениям рекомендуется объединять несколько операций в меньшее количество вызовов runLater. Кроме того, по возможности, длительные операции следует выполнять в фоновом потоке, освобождая поток приложения JavaFX для операций с графическим интерфейсом пользователя.

[...]

Обратите внимание на примечание из приведенной выше документации. Вjavafx.concurent.Task класс избегает этого, объединяя обновления в свои message, progress, а также valueсвойства. В настоящее время это реализовано с помощьюAtomicReferenceи стратегические комплексные операции. Если интересно, можете взглянуть на реализацию (JavaFX с открытым исходным кодом).

Я бы предпочел PauseTransition:

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

Вот решение с использованием 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() {
    // ...
}

По сравнению с решением Сергея вы не посвящаете весь поток получению статуса из сети, а вместо этого используете общий пул потоков для этого.

Вы можете использовать ScheduledService тоже. Я использую эту альтернативу после того, как заметил, что во время использованияTimeline а также PauseTransition произошли некоторые зависания пользовательского интерфейса в моем приложении, особенно когда пользователь взаимодействует с элементами MenuBar(в JavaFX 12). Используя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()
   }
}

Было нелегко найти способ запрограммировать такое поведение, возможно, потому, что мой процесс считывает ввод-вывод, работает за миллисекунды, и я чувствовал, что его часто прерывал поток графического интерфейса, но я сделал это, создав BackgroundProcessкласс и с помощью ScheduledExecutorService.

На стороне управления я использую PauseTransitionчитать volatile(нет разногласий) только информация.

Образец кода :

      public class HelloApplication extends Application {
    final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
    final BackgroundProcess backgroundProcess = new BackgroundProcess();

    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 720, 610);
        HelloController helloController = fxmlLoader.getController();
        helloController.setBackgroundProcess(backgroundProcess);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();

        scheduledExecutor.scheduleWithFixedDelay(
                backgroundProcess,
                0, 111, TimeUnit.MILLISECONDS);
    }
    
    @Override
    public void stop() throws Exception {
        super.stop();
        scheduledExecutor.shutdown();
    }

    ...

}



public class BackgroundProcess implements Runnable{
    volatile String status = "";

    @Override
    public void run() {
       status = newStatus();
    }

    ...

}    


public class HelloController {

    @FXML
    protected void initialize() {

        PauseTransition refresh = new PauseTransition(Duration.millis(111));
        wait.setOnFinished((e) -> {

            statusLabel.setText(backgroundProcess.status);

            refresh.playFromStart();
        });
        refresh.play();
    }

    ...

}

Чтобы прочитать синхронизированную (состязательную) информацию, я использую ScheduledServiceдля подготовки информации и предотвращения прерывания потока JavaFX.

Это более сложный пример кода:

      public class HelloApplication extends Application {
    final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
    final BackgroundProcess backgroundProcess = new BackgroundProcess();

    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 720, 610);
        HelloController helloController = fxmlLoader.getController();
        helloController.setBackgroundProcess(backgroundProcess);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();

        scheduledExecutor.scheduleWithFixedDelay(
                backgroundProcess,
                0, 111, TimeUnit.MILLISECONDS);
    }
    
    @Override
    public void stop() throws Exception {
        super.stop();
        scheduledExecutor.shutdown();
    }

    ...

}



public class BackgroundProcess implements Runnable{
    volatile String status = "";
    LinkedTransferQueue<String> queue = new LinkedTransferQueue();

    @Override
    public void run() {
       status = newStatus();
       addToQueue();
    }

    ...

}    


public class HelloController {


    static class SynchronizedInformation {
        ArrayList<String> list;
    }

    private SynchronizedInformation prepareSynchronizedInformation() {
        if (backgroundProcess.queue.isEmpty()) {
            return null;
        }

        final SynchronizedInformation r = new SynchronizedInformation();
        int size = backgroundProcess.queue.size();
        r.list = new ArrayList<>(size);
        String line;
        while (r.list.size() < size && null != (line = backgroundProcess.queue.poll())) {
            r.list.add(line);
        }

        return r;
    }

    private void refreshSynchronizedInformation(SynchronizedInformation synchronizedInformation) {
        if (null != synchronizedInformation) {
            synchronizedInformation.list.forEach(textArea::appendText);
        }
        statusLabel.setText(backgroundProcess.incoming);
    }

    @FXML
    protected void initialize() {
        ScheduledService<SynchronizedInformation> svc = new ScheduledService<>() {
            @Override
            protected Task<SynchronizedInformation> createTask() {
                return new Task<SynchronizedInformation>() {
                    @Override
                    protected SynchronizedInformation call() throws Exception {
                        return prepareSynchronizedInformation();
                    }
                };
            }
        };
        svc.setDelay(Duration.millis(111));
        svc.setOnSucceeded(e -> refreshSynchronizedInformation(svc.getValue()));
        svc.start();


    ...

}
Другие вопросы по тегам