我尝试定期在JavaFX应用程序后台线程中运行,这会修改一些GUI属性。
我想我知道如何使用Task
中的Service
和javafx.concurrent
类,并且无法在不使用Thread#sleep()
方法的情况下弄清楚如何运行此类定期任务。如果我可以使用Executor
制作方法(Executors
)
Executors.newSingleThreadScheduledExecutor()
,那就太好了
我尝试每5秒运行一次Runnable
,重新启动javafx.concurrent.Service
但会在调用service.restart
甚至service.getState()
时立即挂起。
所以最后我使用Executors.newSingleThreadScheduledExecutor()
,每隔5秒触发Runnable
,Runnable
使用以下Runnable
运行Platform.runLater(new Runnable() {
//here i can modify GUI properties
}
Task
看起来非常讨厌:(使用Service
或{{1}}课程有更好的方法吗?
答案 0 :(得分:92)
您可以使用时间轴:
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没有任何作用),您可以使用旧版java.util.Timer
:
new Timer().schedule(
new TimerTask() {
@Override
public void run() {
System.out.println("ping");
}
}, 0, 5000);
答案 1 :(得分:17)
前言:这个问题通常是重复的目标,这些问题询问如何在JavaFX中执行定期操作,无论该操作是否应在后台执行。尽管该问题已经有了不错的答案,但是该答案试图将所有给定的信息(以及更多信息)整合为一个答案,并说明/显示每种方法之间的差异。
此答案的重点是JavaSE和JavaFX中可用的API,而不是第三方库(例如ReactFX(在Tomas Mikula's answer中展示))。
与大多数主流GUI框架一样,JavaFX是单线程的。这意味着只有一个线程专门用于读取和写入UI状态以及处理用户生成的事件(例如鼠标事件,键事件等)。在JavaFX中,此线程称为“ JavaFX Application Thread”,有时简称为“ FX thread”,但其他框架可能将其称为其他名称。其他一些名称包括“ UI线程”,“事件调度线程”和“主线程”。
绝对重要的是,只有在 JavaFX Application Thread 上访问或操纵与屏幕上显示的GUI相连的任何内容。 JavaFX框架不是线程安全的,使用其他线程来不正确地读取或写入UI状态可能会导致未定义的行为。即使您看不到任何外部可见的问题,在没有necessary synchronization的情况下访问线程之间共享的状态也会破坏代码。
但是,只要它们不是“活动的”,就可以在任何线程上操纵许多GUI对象。来自javafx.scene.Node
的文档:
节点对象可以在任何线程上构造和修改,只要它们尚未附加到
showing
的Scene
中的Window
> [加重] 。应用程序必须将节点附加到此类场景,或在JavaFX应用程序线程上对其进行修改。
但是其他GUI对象,例如Window
甚至是Node
的某些子类(例如WebView
),都更加严格。例如,来自javafx.stage.Window
的文档:
必须在JavaFX Application Thread上构造和修改Window对象。
如果不确定GUI对象的线程规则,则其文档应提供所需的信息。
由于JavaFX是单线程的,因此您还必须确保不要阻塞或独占FX线程。如果线程不能自由地执行其工作,则永远不会重绘UI,并且无法处理用户生成的新事件。不遵循此规则可能会导致臭名昭著的无响应/冻结的用户界面,并且用户不满意。
sleep JavaFX应用程序线程实际上总是错误的。
至少有此答案的目的,有两种不同类型的定期任务:
如果您的周期性任务又短又简单,那么使用后台线程就显得过头了,只会增加不必要的复杂性。更合适的解决方案是使用javafx.animation
API。动画是异步的,但完全保留在 JavaFX Application Thread 中。换句话说,动画提供了一种在FX线程上“循环”的方法,每次迭代之间都有延迟,而无需实际使用循环。
共有三类特别适合于定期前台任务。
Timeline
由一个或多个KeyFrame
组成。每个KeyFrame
都有一个指定的完成时间。每个人也可以有一个“完成”处理程序,该处理程序在指定的时间量过去之后被调用。这意味着您可以创建一个具有单个Timeline
的{{1}},该KeyFrame
定期执行一次动作,looping次数不限(包括forever)。
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
,时间为2秒,然后有另一个KeyFrame
,时间为2秒,则两个KeyFrame
将在动画开始后两秒钟完成。要使第二个KeyFrame
在第一个SequentialTransition
之后第二秒完成,其时间必须为 4 秒。
与其他动画类不同,PauseTransition
并不用于对任何东西进行动画。它的主要目的是用作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
属性无法使用,因为未完成的处理程序不会在每个周期的末尾被调用,而是仅在 last 周期的末尾被调用。 Timeline
也是如此;之所以可以与上面的Timeline
一起使用,是因为完成的处理程序不是向KeyFrame
注册,而是向cycleCount
注册。
由于PauseTransition
属性无法用于playFromStart()
多个周期,因此仅循环一定次数(而不是永远)变得更加困难。您必须自己跟踪状态,并且仅在适当的时候调用Animation
。请记住,在lambda表达式或匿名类之外声明但在所述lambda表达式或匿名类内部使用的局部变量必须是最终的或有效地是最终的。
AnimationTimer
类是JavaFX动画API的最低级别。它不是AnimationTimer
的子类,因此没有上面使用的任何属性。相反,它有一个抽象方法,当启动计时器时,每帧以当前帧的时间戳(以纳秒为单位)#handle(long)
被调用一次。为了使用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
或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"
}
};
}
}
}
。
如果您的定期任务很耗时(例如,昂贵的计算)或阻塞(例如,I / O),则需要使用后台线程。 JavaFX内置了一些并发实用程序,以辅助后台线程和FX线程之间的通信。这些实用程序的描述如下:
javafx.concurrent
包中类的文档。对于需要与FX线程通信的定期后台任务,要使用的类为javafx.concurrent.ScheduledService
。该类将定期执行其任务,并根据指定的时间段在成功执行后重新启动。如果这样做,它甚至会在执行失败后重试可配置的次数。
ScheduledService
以下是ScheduledService
文档中的注释:
此类的时间并不是绝对可靠的。一个非常繁忙的事件线程可能会在后台Task的执行开始时引入一些时间延迟,因此,周期或延迟的很小值可能不准确。几百毫秒或更长的延迟或周期应该是相当可靠的。
另一个:
lastValue
引入了一个名为lastValue
的新属性。Service
是最后成功计算的值。因为value
在每次运行时都会清除其ScheduledService
属性,并且因为value
将在完成后立即重新安排运行(除非它进入已取消或失败状态),因此{{1} }属性在ScheduledService
上并不太有用。在大多数情况下,您将需要使用lastValue
返回的值。
最后一个注释表示绑定到value
的{{1}}属性是无用的。尽管查询了ScheduledService
属性,但上面的示例仍然有效,因为在重新安排服务之前,该属性是在value
处理程序中查询的。
如果定期后台任务不需要与UI交互,则可以改用Java的标准API。更具体地说,可以:
java.util.Timer
类(不是 onSucceeded
),java.util.concurrent.ScheduledExecutorService
界面。请注意,javax.swing.Timer
支持线程池,而ScheduledExecutorService
仅支持单个线程。
如果出于某种原因您不能使用Timer
,但是无论如何需要与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操作。
[...]
注意上述文档中的注释。 ScheduledService
类通过合并对其javafx.concurent.Task
,message
和progress
属性的更新来避免这种情况。当前,这是通过使用value
和战略性的获取和设置操作来实现的。如果有兴趣,可以看一下实现(JavaFX是open source)。
答案 2 :(得分:11)
我更喜欢PauseTransition:
PauseTransition wait = new PauseTransition(Duration.seconds(5));
wait.setOnFinished((e) -> {
/*YOUR METHOD*/
wait.playFromStart();
});
wait.play();
答案 3 :(得分:6)
这是使用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的解决方案相比,您不会将整个线程专门用于从网络获取状态,而是使用共享线程池。
答案 4 :(得分:1)
您也可以使用ScheduledService
。在注意到Timeline
和PauseTransition
的使用过程中,我在应用程序中发生了一些UI冻结,尤其是当用户与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()
}
}