当前位置: 首页 > 面试题库 >

JavaFX GUI Updater实用程序类中的并发问题

薛望
2023-03-14
问题内容

我正在JavaFX中为大型Java项目构建GUI。这个项目有许多不同的工作线程在后台执行一些繁重的计算,我试图在GUI中可视化这些工作线程的进度。所谓进步,是指不仅是裸露的百分比,而且是Task类中未包含的其他变量,例如:

  • 当前文件
  • 当前错误计数
  • 到目前为止读取的字节数

由于这些进度变量的变化非常快,并且由于必须从JavaFX线程(Platform.runLater())执行GUI更新,因此JavaFX事件队列很快就会过载。我正在尝试通过构建实用程序类来解决此问题,该实用程序类能够从JavaFX线程外部异步更新GUI属性。应该跳过快速连续的更新,以便仅显示最新值,从而避免用Runnables挤满JavaFX事件队列。

因此,我构建了以下类GUIUpdater以将Properties(通常是GUI元素,如Label)绑定到ObservableValues(例如SimpleStringProperty)。此类具有两个InnerClasses:

  • PropertyUpdater负责将单个Property绑定到单个ObservableValue并进行更新。
  • Updater提供了用于Platform.runLater可重复使用的Runnable对象()。

实用程序类:

package main;

import java.util.concurrent.ConcurrentLinkedQueue;

import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

/**
 * Class for enabling fast updates of GUI components from outside the JavaFX thread.
 *  Updating GUI components (such as labels) should be done from the JavaFX thread by using Platform.runLater for example.
 *  This makes it hard to update the GUI with a fast changing variable as it is very easy to fill up the JavaFX event queue faster than it can be emptied (i.e. faster than it can be drawn).
 *  This class binds ObservableValues to (GUI) Properties and ensures that quick consecutive updates are ignored, only updating to the latest value.
 */
public class GUIUpdater {
    private ConcurrentLinkedQueue<PropertyUpdater<?>>   dirtyPropertyUpdaters   =   new ConcurrentLinkedQueue<>();
    private Updater                                     updater                 =   new Updater();
    private boolean                                     isUpdating              =   false;

    /**
     * Binds an ObservableValue to a Property.
     *  Updates to the ObservableValue can be made from outside the JavaFX thread and the latest update will be reflected in the Property.
     * @param property      (GUI) Property to be updated/
     * @param observable    ObservableValue to update the GUI property to.
     */
    public <T> void bind(Property<T> property, ObservableValue<T> observable) {
        PropertyUpdater<T>  propertyUpdater = new PropertyUpdater<>(property, observable);
        observable.addListener(propertyUpdater);
    }

    /**
     * Unbinds the given ObservableValue from the given Property.
     *  Updates to the ObservableValue will no longer be reflected in the Property.
     * @param property      (GUI) Property to unbind the ObservableValue from.
     * @param observable    ObservableValue to unbind from the given Property.
     */
    public <T> void unbind(Property<T> property, ObservableValue<T> observable) {
        PropertyUpdater<T>  tmpPropertyUpdater = new PropertyUpdater<>(property, observable);
        observable.removeListener(tmpPropertyUpdater);
    }

    /**
     * Schedules an update to the GUI by using a call to Platform.runLater().
     *  The updated property is added to the dirtyProperties list, marking it for the next update round.
     *  Will only submit the event to the event queue if the event isn't in the event queue yet.
     * @param updater
     */
    private void scheduleUpdate(PropertyUpdater<?> updater) {
        this.dirtyPropertyUpdaters.add(updater);

        // Make sure the isUpdating var isn't changed concurrently by the Updater thread (on the JavaFX event queue)
        synchronized (this) {
            if (!this.isUpdating) {
                this.isUpdating = true;
                Platform.runLater(this.updater);
            }
        }
    }

    /**
     * Class used for binding a single ObservableValue to a Property and updating it.
     *
     * @param <T>
     */
    private class PropertyUpdater<T> implements ChangeListener<T> {
        private boolean             isDirty     =   false;
        private Property<T>         property    =   null;
        private ObservableValue<T>  observable  =   null;

        public PropertyUpdater(Property<T> property, ObservableValue<T> observable) {
            this.property = property;
            this.observable = observable;
        }

        @Override
        /**
         * Called whenever the ObservableValue has changed. Marks this Updater as dirty.
         */
        public synchronized void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) {
            if (!this.isDirty) {
                this.isDirty = true;
                GUIUpdater.this.scheduleUpdate(this);
            }
        }

        /**
         * Updates the Property to the ObservableValue and marks it as clean again.
         *  Should only be called from the JavaFX thread.
         */
        public synchronized void update() {
            T value = this.observable.getValue();
            this.property.setValue(value);
            this.isDirty = false;
        }

        @Override
        /**
         * Two PropertyUpdaters are equals if their Property and ObservableValue map to the same object (address).
         */
        public boolean equals(Object otherObj) {
            PropertyUpdater<?>  otherUpdater = (PropertyUpdater<?>) otherObj;
            if (otherObj == null) {
                return false;
            } else {
                // Only compare addresses (comparing with equals also compares contents):
                return (this.property == otherUpdater.property) && (this.observable == otherUpdater.observable);
            }
        }
    }

    /**
     * Simple class containing the Runnable for the call to Platform.runLater.
     *  Hence, the run() method should only be called from the JavaFX thread.
     *
     */
    private class Updater implements Runnable {

        @Override
        public void run() {
            // Loop through the individual PropertyUpdaters, updating them one by one:
            while(!GUIUpdater.this.dirtyPropertyUpdaters.isEmpty()) {
                PropertyUpdater<?>  curUpdater = GUIUpdater.this.dirtyPropertyUpdaters.poll();
                curUpdater.update();
            }

            // Make sure we're not clearing the mark when scheduleUpdate() is still setting it:
            synchronized (GUIUpdater.this) {
                GUIUpdater.this.isUpdating = false;
            }
        }

    }
}

这是测试GUIUpdater实用程序类的简单类:

package main;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

public class JavaFXTest extends Application {
    private GUIUpdater  guiUpdater  =   new GUIUpdater();
    private Label       lblState    =   new Label();
    private ProgressBar prgProgress =   new ProgressBar();

    public static void main(String args[]) {
        JavaFXTest.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        // Init window:
        FlowPane    flowPane = new FlowPane();
        primaryStage.setScene(new Scene(flowPane));
        primaryStage.setTitle("JavaFXTest");

        // Add a Label and a progressBar:
        flowPane.getChildren().add(this.lblState);
        flowPane.getChildren().add(this.prgProgress);

        // Add button:
        Button  btnStart = new Button("Start");
        btnStart.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                // Create task:
                TestTask    testTask = new TestTask();

                // Bind:
                JavaFXTest.this.guiUpdater.bind(JavaFXTest.this.lblState.textProperty(), testTask.myStateProperty());
                JavaFXTest.this.prgProgress.progressProperty().bind(testTask.progressProperty());   // No need to use GUIUpdater here, Task class provides the same functionality for progress.

                // Start task:
                Thread  tmpThread = new Thread(testTask);
                tmpThread.start();
            }
        });
        flowPane.getChildren().add(btnStart);

        // Show:
        primaryStage.show();
    }

    /**
     * A simple task containing a for loop to simulate a fast running and fast updating process.
     * @author DePhille
     *
     */
    private class TestTask extends Task<Void> {
        private SimpleStringProperty    myState =   new SimpleStringProperty();

        @Override
        protected Void call() throws Exception {

            // Count:
            try {
                int maxValue = 1000000;

                System.out.println("Starting...");
                for(int i = 0; i < maxValue; i++) {
                    this.updateProgress(i, maxValue - 1);
                    this.myState.set("Hello " + i);
                }
                System.out.println("Done!");    
            } catch(Exception e) {
                e.printStackTrace();
            }

            // Unbind:
            JavaFXTest.this.guiUpdater.unbind(JavaFXTest.this.lblState.textProperty(), this.myStateProperty());
            return null;
        }

        public SimpleStringProperty myStateProperty() {
            return this.myState;
        }

    }

}

代码的问题是,有时Label不会更新为最新值(在这种情况下为999999)。似乎大多数情况是在应用程序启动后立即发生的,因此,启动应用程序,单击“开始”按钮,关闭它,然后重复此过程,应在尝试几次后重现该问题。据我所知,我synchronized在需要的地方添加了块,这就是为什么我不明白问题出在哪里的原因。

即使我主要是在寻找所描述问题的解决方案,也非常感谢所有建议(甚至那些与问题无关的建议)!我也在代码中添加了注释,因此,我希望与上面的信息一起提供有关问题和代码的足够详细信息。

提前致谢!


问题答案:

我自己可以解决此问题。System.out在各个地方添加几天后,结果发现问题出在isUpdating变量的并发问题上。当JavaFX线程位于while循环和中的synchronized块之间时,发生了问题Updater.run。我通过使Updater.runGUIUpdater.scheduleUpdate方法在同一对象上同步来解决了这个问题。

我也将其GUIUpdater制成了仅静态对象,因为Runnables无论其他GUIUpdater实例如何,都有多个实例将放置在JavaFX事件队列中,从而阻塞了事件队列。总而言之,这是结果GUIUpdater类:

package be.pbeckers.javafxguiupdater;

import java.util.concurrent.ConcurrentLinkedQueue;

import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;


/**
 * Class for enabling fast updates of GUI components from outside the JavaFX thread.
 *  Updating GUI components (such as labels) should be done from the JavaFX thread by using Platform.runLater for example.
 *  This makes it hard to update the GUI with a fast changing variable as it is very easy to fill up the JavaFX event queue faster than it can be emptied (i.e. faster than it can be drawn).
 *  This class binds ObservableValues to (GUI) Properties and ensures that quick consecutive updates are ignored, only updating to the latest value.
 */
public abstract class GUIUpdater {
    private static  ConcurrentLinkedQueue<PropertyUpdater<?>>   dirtyPropertyUpdaters   =   new ConcurrentLinkedQueue<>();
    private static  Updater                                     updater                 =   new Updater();
    private static  boolean                                     isUpdating              =   false;

    /**
     * Binds an ObservableValue to a Property.
     *  Updates to the ObservableValue can be made from outside the JavaFX thread and the latest update will be reflected in the Property.
     * @param property      (GUI) Property to be updated/
     * @param observable    ObservableValue to update the GUI property to.
     */
    public static <T> void bind(Property<T> property, ObservableValue<T> observable) {
        PropertyUpdater<T>  propertyUpdater = new PropertyUpdater<>(property, observable);
        observable.addListener(propertyUpdater);
    }

    /**
     * Unbinds the given ObservableValue from the given Property.
     *  Updates to the ObservableValue will no longer be reflected in the Property.
     * @param property      (GUI) Property to unbind the ObservableValue from.
     * @param observable    ObservableValue to unbind from the given Property.
     */
    public static <T> void unbind(Property<T> property, ObservableValue<T> observable) {
        PropertyUpdater<T>  tmpPropertyUpdater = new PropertyUpdater<>(property, observable);
        observable.removeListener(tmpPropertyUpdater);
    }

    /**
     * Schedules an update to the GUI by using a call to Platform.runLater().
     *  The updated property is added to the dirtyProperties list, marking it for the next update round.
     *  Will only submit the event to the event queue if the event isn't in the event queue yet.
     * @param updater
     */
    private static synchronized void scheduleUpdate(PropertyUpdater<?> updater) {
        GUIUpdater.dirtyPropertyUpdaters.add(updater);

        if (!GUIUpdater.isUpdating) {
            GUIUpdater.isUpdating = true;
            Platform.runLater(GUIUpdater.updater);
        }
    }

    /**
     * Class used for binding a single ObservableValue to a Property and updating it.
     *
     * @param <T>
     */
    private static class PropertyUpdater<T> implements ChangeListener<T> {
        private boolean             isDirty     =   false;
        private Property<T>         property    =   null;
        private ObservableValue<T>  observable  =   null;

        public PropertyUpdater(Property<T> property, ObservableValue<T> observable) {
            this.property = property;
            this.observable = observable;
        }

        @Override
        /**
         * Called whenever the ObservableValue has changed. Marks this Updater as dirty.
         */
        public synchronized void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) {
            if (!this.isDirty) {
                this.isDirty = true;
                GUIUpdater.scheduleUpdate(this);
            }
        }

        /**
         * Updates the Property to the ObservableValue and marks it as clean again.
         *  Should only be called from the JavaFX thread.
         */
        public synchronized void update() {
            T value = this.observable.getValue();
            this.property.setValue(value);
            this.isDirty = false;
        }

        @Override
        /**
         * Two PropertyUpdaters are equals if their Property and ObservableValue map to the same object (address).
         */
        public boolean equals(Object otherObj) {
            PropertyUpdater<?>  otherUpdater = (PropertyUpdater<?>) otherObj;
            if (otherObj == null) {
                return false;
            } else {
                // Only compare addresses (comparing with equals also compares contents):
                return (this.property == otherUpdater.property) && (this.observable == otherUpdater.observable);
            }
        }
    }

    /**
     * Simple class containing the Runnable for the call to Platform.runLater.
     *  Hence, the run() method should only be called from the JavaFX thread.
     *
     */
    private static class Updater implements Runnable {

        @Override
        public void run() {
            synchronized (GUIUpdater.class) {

                // Loop through the individual PropertyUpdaters, updating them one by one:
                while(!GUIUpdater.dirtyPropertyUpdaters.isEmpty()) {
                    PropertyUpdater<?>  curUpdater = GUIUpdater.dirtyPropertyUpdaters.poll();
                    curUpdater.update();
                }

                // Mark as updated:
                GUIUpdater.isUpdating = false;              
            }
        }

    }
}

这是测试器类的稍有更新的版本(由于它完全不重要,因此我不对其进行详细介绍):

package be.pbeckers.javafxguiupdater.test;

import be.pbeckers.javafxguiupdater.GUIUpdater;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

public class JavaFXTest extends Application {
    private Label       lblCurFile      =   new Label();
    private Label       lblErrors       =   new Label();
    private Label       lblBytesParsed  =   new Label();
    private ProgressBar prgProgress     =   new ProgressBar();

    public static void main(String args[]) {
        JavaFXTest.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        // Init window:
        FlowPane    flowPane = new FlowPane();
        primaryStage.setScene(new Scene(flowPane));
        primaryStage.setTitle("JavaFXTest");

        // Add a few Labels and a progressBar:
        flowPane.getChildren().add(this.lblCurFile);
        flowPane.getChildren().add(this.lblErrors);
        flowPane.getChildren().add(this.lblBytesParsed);
        flowPane.getChildren().add(this.prgProgress);

        // Add button:
        Button  btnStart = new Button("Start");
        btnStart.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                // Create task:
                TestTask    testTask = new TestTask();

                // Bind:
                GUIUpdater.bind(JavaFXTest.this.lblCurFile.textProperty(), testTask.curFileProperty());
                GUIUpdater.bind(JavaFXTest.this.lblErrors.textProperty(), testTask.errorsProperty());
                GUIUpdater.bind(JavaFXTest.this.lblBytesParsed.textProperty(), testTask.bytesParsedProperty());
                JavaFXTest.this.prgProgress.progressProperty().bind(testTask.progressProperty());   // No need to use GUIUpdater here, Task class provides the same functionality for progress.

                // Start task:
                Thread  tmpThread = new Thread(testTask);
                tmpThread.start();
            }
        });
        flowPane.getChildren().add(btnStart);

        // Show:
        primaryStage.show();
    }

    /**
     * A simple task containing a for loop to simulate a fast running and fast updating process.
     * @author DePhille
     *
     */
    private class TestTask extends Task<Void> {
        private SimpleStringProperty    curFile     =   new SimpleStringProperty();
        private SimpleStringProperty    errors      =   new SimpleStringProperty();
        private SimpleStringProperty    bytesParsed =   new SimpleStringProperty();

        @Override
        protected Void call() throws Exception {

            // Count:
            try {
                int maxValue = 1000000;
                long startTime = System.currentTimeMillis();

                System.out.println("Starting...");
                for(int i = 0; i < maxValue; i++) {
                    this.updateProgress(i, maxValue - 1);

                    // Simulate some progress variables:
                    this.curFile.set("File_" + i + ".txt");
                    if ((i % 1000) == 0) {
                        //this.errors.set("" + (i / 1000) + " Errors");
                    }
                    //this.bytesParsed.set("" + (i / 1024) + " KBytes");
                }
                long stopTime = System.currentTimeMillis();
                System.out.println("Done in " + (stopTime - startTime) + " msec!");
            } catch(Exception e) {
                e.printStackTrace();
            }

            // Unbind:
            GUIUpdater.unbind(JavaFXTest.this.lblCurFile.textProperty(), this.curFileProperty());
            GUIUpdater.unbind(JavaFXTest.this.lblErrors.textProperty(), this.errorsProperty());
            GUIUpdater.unbind(JavaFXTest.this.lblBytesParsed.textProperty(), this.bytesParsedProperty());
            return null;
        }

        public SimpleStringProperty curFileProperty() {
            return this.curFile;
        }

        public SimpleStringProperty errorsProperty() {
            return this.errors;
        }

        public SimpleStringProperty bytesParsedProperty() {
            return this.bytesParsed;
        }

    }

}


 类似资料:
  • 如果我有一个有状态类,它需要像无状态类这样的实用程序来对其执行操作。这些有状态类保存在容器(有状态)类的列表中。这就是我对纯Java所做的: 在里面是这样的过程: 现在有了JavaEE CDI,我会做: 在这种情况下,所有StatefulBean都可以访问无状态Bean的共享池,而不会出现并发问题,并且可以根据请求进行适当的扩展。 然而,由于不是托管bean,我无法将其注入,因此我使用了实用程序类

  • 我们正在开发一个部署在Websphere Web Server上的Java EE Web应用程序。目前,需要并发执行来加快响应时间。那么在下面的选择中,哪个以及为什么会是一个更好的选择呢? 使用 使用(上述两种方法的基本用法和实现差异是什么) 还有其他选择吗?

  • 为啥最后打印类似100475,100425之类的值? 我看写这个的人说会打印200000.

  • 本文向大家介绍Python中的并发编程实例,包括了Python中的并发编程实例的使用技巧和注意事项,需要的朋友参考一下 一、简介   我们将一个正在运行的程序称为进程。每个进程都有它自己的系统状态,包含内存状态、打开文件列表、追踪指令执行情况的程序指针以及一个保存局部变量的调用栈。通常情况下,一个进程依照一个单序列控制流顺序执行,这个控制流被称为该进程的主线程。在任何给定的时刻,一个程序只做一件事

  • 问题内容: 程序顺序规则指出:“线程中的每个动作都发生在该线程中的每个动作之后,程序顺序之后” 我在另一个线程中读到一个 动作 是 读取和写入变量 显示器的锁定和解锁 用线程开始和加入 这是否意味着可以按顺序更改读取和写入,但不能通过第二行或第三行中指定的操作更改读取和写入操作的顺序? 2.“程序顺序”是什么意思? 举例说明将非常有帮助。 其他相关问题 假设我有以下代码: 首先,它是一个单线程应用

  • 我希望有人能帮我,因为我在这里迷路了。我正在尝试使用线程在我们的web应用程序中创建一些报告。我们的应用程序使用wildfly-10-final、postgresql、zk框架和ejb3。我按照此示例创建线程。 我在无状态服务中创建了此函数: 我使用此行调用ManagedExecutorService: 它在domain-clustered.xml有这样的配置: 此函数是从viewmodel调用的