JavaFX GUI更新器实用程序类中的并发问题 [英] Concurrency issues in JavaFX GUI Updater utility class

查看:1021
本文介绍了JavaFX GUI更新器实用程序类中的并发问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在为一个相当大的Java项目在JavaFX中构建一个GUI。这个项目有许多不同的工作线程在后台做一些沉重的计算,我试图在GUI中可视化这些工作线程的进度。进度我的意思不仅是一个裸百分比,而且还包括Task类中不包含的其他变量,例如:





  • 当前错误计数

  • 到目前为止读取的字节数

  • li>


由于这些进度变量变化非常快,因为我必须从JavaFX线程(Platform.runLater())执行GUI更新, JavaFX事件队列很快重载。我试图通过构建一个实用程序类能够从JavaFX线程外部异步更新GUI属性来解决这个问题。应该跳过快速的连续更新,以便只显示最新的值,从而避免使用Runnables群集JavaFX事件队列。



因此,我构建了下面的类 GUIUpdater 将属性(通常是GUI元素,如Label)绑定到ObservableValues(如SimpleStringProperty)。这个类有两个InnerClasses:




  • PropertyUpdater

  • Updater 为Platform.runLater()提供了一个可重复使用的Runnable对象。



实用程序类:

  ; 

import java.util.concurrent.ConcurrentLinkedQueue;

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

/ **
*用于从JavaFX线程外部快速更新GUI组件的类。
*更新GUI组件(例如标签)应该使用Platform.runLater从JavaFX线程完成。
*这使得使用快速变化的变量更新GUI变得很困难,因为它很容易填充JavaFX事件队列比它可以被清空(即比它可以绘制得更快)。
*此类将ObservableValues绑定到(GUI)属性,并确保快速连续更新被忽略,只更新到最新的值。
* /
public class GUIUpdater {
private ConcurrentLinkedQueue< PropertyUpdater<?> dirtyPropertyUpdaters = new ConcurrentLinkedQueue<>();
private Updater updater = new Updater();
private boolean isUpdating = false;

/ **
*将一个ObservableValue绑定到一个属性。
*可以从JavaFX线程外部对ObservableValue进行更新,并且最新更新将反映在属性中。
* @param属性(GUI)要更新的属性/
* @param observable可更新GUI属性的ObservableValue。
* /
public< T> void bind(Property< T> property,ObservableValue< T> observable){
PropertyUpdater< T> propertyUpdater = new PropertyUpdater<>(property,observable);
observable.addListener(propertyUpdater);
}

/ **
*从给定的Property中取消绑定给定的ObservableValue。
*对ObservableValue的更新将不再反映在属性中。
* @param属性(GUI)解除ObservableValue的属性。
* @param observable ObservableValue从给定属性解除绑定。
* /
public< T> void unbind(Property< T> property,ObservableValue< T> observable){
PropertyUpdater< T> tmpPropertyUpdater = new PropertyUpdater<>(property,observable);
observable.removeListener(tmpPropertyUpdater);
}

/ **
*通过调用Platform.runLater()来计划对GUI的更新。
*更新的属性被添加到dirtyProperties列表,标记它用于下一个更新轮次。
*如果事件不在事件队列中,则只将事件提交到事件队列。
* @param updater
* /
private void scheduleUpdate(PropertyUpdater<?> updater){
this.dirtyPropertyUpdaters.add(updater);

//确保isUpdating var不会被Updater线程同时更改(在JavaFX事件队列上)
synchronized(this){
if(!this.isUpdating ){
this.isUpdating = true;
Platform.runLater(this.updater);
}
}
}

/ **
*用于将单个ObservableValue绑定到Property并更新它的类。
*
* @param< T>
* /
private class PropertyUpdater< T>实现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
/ **
*当ObservableValue已更改时调用。将此更新器标记为脏。
* /
public synchronized void changed(ObservableValue< ;? extends T> observable,T oldValue,T newValue){
if(!this.isDirty){
this.isDirty =真正;
GUIUpdater.this.scheduleUpdate(this);
}
}

/ **
*将属性更新为ObservableValue,并将其标记为干净。
*应该只从JavaFX线程调用。
* /
public synchronized void update(){
T value = this.observable.getValue();
this.property.setValue(value);
this.isDirty = false;
}

@Override
/ **
*如果Property和ObservableValue映射到同一个对象(地址),则两个PropertyUpdaters是等价的。
* /
public boolean equals(Object otherObj){
PropertyUpdater<?> otherUpdater =(PropertyUpdater<?>)otherObj;
if(otherObj == null){
return false;
} else {
//只比较地址(与equals比较也比较内容):
return(this.property == otherUpdater.property)&& (this.observable == otherUpdater.observable);
}
}
}

/ **
*包含用于调用Platform.runLater的Runnable的简单类。
*因此,run()方法应该只从JavaFX线程调用。
*
* /
private class Updater implements Runnable {

@Override
public void run(){
//循环遍历单独的PropertyUpdaters,逐个更新它们:
while(!GUIUpdater.this.dirtyPropertyUpdaters.isEmpty()){
PropertyUpdater<?& curUpdater = GUIUpdater.this.dirtyPropertyUpdaters.poll();
curUpdater.update();
}

//当scheduleUpdate()仍然设置它时,确保我们不清除标记:
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应用程序{
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);

//添加一个Label和一个progressBar:
flowPane.getChildren()。add(this.lblState);
flowPane.getChildren()。add(this.prgProgress);

//添加按钮:
Button btnStart = new Button(Start);
btnStart.setOnAction(new EventHandler< ActionEvent>(){

@Override
public void handle(ActionEvent event){
//创建任务:
TestTask testTask = new TestTask();

// Bind:
JavaFXTest.this.guiUpdater.bind(JavaFXTest.this.lblState.textProperty(),testTask.myStateProperty());
JavaFXTest.this.prgProgress.progressProperty()。bind(testTask.progressProperty()); //不需要使用GUIUpdater这里,Task类提供了相同的进度功能

/ /启动任务:
线程tmpThread =新线程(testTask);
tmpThread.start();
}
});
flowPane.getChildren()。add(btnStart);

//显示:
primaryStage.show();
}

/ **
*一个包含for循环的简单任务,用于模拟快速运行和快速更新过程。
* @author DePhille
*
* /
私有类TestTask extends Task< Void> {
private SimpleStringProperty myState = new SimpleStringProperty();

@Override
protected Void call()throws Exception {

//计数:
try {
int maxValue = 1000000;

System.out.println(Starting ...);
for(int i = 0; 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;
}

}

}

代码的问题是,有时标签不会更新为最新值(在这种情况下为999999)。它似乎大多发生在应用程序启动后,所以启动应用程序,单击开始按钮,关闭它,并重复这个过程应该在几个尝试后复制该问题。就我可以看到,我已经添加了同步块在哪里需要这就是为什么我不明白问题来自哪里。



即使我主要寻找解决问题的所有建议,非常感谢(甚至那些不相关的问题)!我在代码中添加了注释,所以我希望与上面的信息一起提供足够的细节问题和代码。



提前感谢! p>

解决方案

我可以自己解决问题。在多个地方添加 System.out 几天后,问题是由于 isUpdating 变量。当JavaFX线程位于 while 循环和更新器中的同步块之间时, .run 。我通过同时在同一对象上同步 Updater.run GUIUpdater.scheduleUpdate 方法解决了这个问题。 p>

我也将 GUIUpdater 变成一个静态对象,因为有多个实例会放置 Runnables 在JavaFX事件队列中,而不管其他 GUIUpdater 实例,阻塞事件队列。总而言之,这是产生的 GUIUpdater 类:

  .pbeckers.javafxguiupdater; 

import java.util.concurrent.ConcurrentLinkedQueue;

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


/ **
*允许从JavaFX线程外部快速更新GUI组件的类。
*更新GUI组件(例如标签)应该使用Platform.runLater从JavaFX线程完成。
*这使得使用快速变化的变量更新GUI变得很困难,因为它很容易填充JavaFX事件队列比它可以被清空(即比它可以绘制得更快)。
*此类将ObservableValues绑定到(GUI)属性,并确保快速连续更新被忽略,只更新到最新的值。
* /
public abstract class GUIUpdater {
private static ConcurrentLinkedQueue< PropertyUpdater<?> dirtyPropertyUpdaters = new ConcurrentLinkedQueue<>();
private static Updater updater = new Updater();
private static boolean isUpdating = false;

/ **
*将一个ObservableValue绑定到一个属性。
*可以从JavaFX线程外部对ObservableValue进行更新,并且最新更新将反映在属性中。
* @param属性(GUI)要更新的属性/
* @param observable可更新GUI属性的ObservableValue。
* /
public static< T> void bind(Property< T> property,ObservableValue< T> observable){
PropertyUpdater< T> propertyUpdater = new PropertyUpdater<>(property,observable);
observable.addListener(propertyUpdater);
}

/ **
*从给定的Property中取消绑定给定的ObservableValue。
*对ObservableValue的更新将不再反映在属性中。
* @param属性(GUI)解除ObservableValue的属性。
* @param observable ObservableValue从给定属性解除绑定。
* /
public static< T> void unbind(Property< T> property,ObservableValue< T> observable){
PropertyUpdater< T> tmpPropertyUpdater = new PropertyUpdater<>(property,observable);
observable.removeListener(tmpPropertyUpdater);
}

/ **
*通过调用Platform.runLater()来计划对GUI的更新。
*更新的属性被添加到dirtyProperties列表,标记它用于下一个更新轮次。
*如果事件不在事件队列中,则只将事件提交到事件队列。
* @param updater
* /
private static synchronized void scheduleUpdate(PropertyUpdater<?> updater){
GUIUpdater.dirtyPropertyUpdaters.add(updater);

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

/ **
*用于将单个ObservableValue绑定到Property并更新它的类。
*
* @param< T>
* /
private static class PropertyUpdater< T>实现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
/ **
*当ObservableValue已更改时调用。将此更新器标记为脏。
* /
public synchronized void changed(ObservableValue< ;? extends T> observable,T oldValue,T newValue){
if(!this.isDirty){
this.isDirty =真正;
GUIUpdater.scheduleUpdate(this);
}
}

/ **
*将属性更新为ObservableValue,并将其标记为干净。
*应该只从JavaFX线程调用。
* /
public synchronized void update(){
T value = this.observable.getValue();
this.property.setValue(value);
this.isDirty = false;
}

@Override
/ **
*如果Property和ObservableValue映射到同一个对象(地址),则两个PropertyUpdaters是等价的。
* /
public boolean equals(Object otherObj){
PropertyUpdater<?> otherUpdater =(PropertyUpdater<?>)otherObj;
if(otherObj == null){
返回false;
} else {
//只比较地址(与equals比较也比较内容):
return(this.property == otherUpdater.property)&& (this.observable == otherUpdater.observable);
}
}
}

/ **
*包含用于调用Platform.runLater的Runnable的简单类。
*因此,run()方法应该只从JavaFX线程调用。
*
* /
私人静态类Updater实现Runnable {

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

//循环遍历各个PropertyUpdaters,逐一更新它们:
while(!GUIUpdater.dirtyPropertyUpdaters.isEmpty()){
PropertyUpdater<?& curUpdater = GUIUpdater.dirtyPropertyUpdaters.poll();
curUpdater.update();
}

//标记为更新:
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应用程序{
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);

//添加几个标签和progressBar:
flowPane.getChildren()。add(this.lblCurFile);
flowPane.getChildren()。add(this.lblErrors);
flowPane.getChildren()。add(this.lblBytesParsed);
flowPane.getChildren()。add(this.prgProgress);

//添加按钮:
Button btnStart = new Button(Start);
btnStart.setOnAction(new EventHandler< ActionEvent>(){

@Override
public void handle(ActionEvent event){
//创建任务:
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()); //不需要使用GUIUpdater这里,Task类提供了相同的进度功能

//开始任务:
Thread tmpThread = new Thread(testTask);
tmpThread.start();
}
});
flowPane.getChildren()。add(btnStart);

//显示:
primaryStage.show();
}

/ **
*一个包含for循环的简单任务,用于模拟快速运行和快速更新过程。
* @author DePhille
*
* /
私有类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 {

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

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

//模拟一些进度变量:
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();
}

//解除绑定:
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;
}

}

}


I'm building a GUI in JavaFX for a rather large Java project. This project has many different worker threads doing some heavy computations in the background and I'm trying to visualize the progress of these worker threads in a GUI. By progress I mean not only a bare percentage but also other variables not contained within the Task class such as (for example):

  • The current file
  • The current Error count
  • The amount of bytes read so far
  • ...

As these progress variables change very rapidly and because I have to do the GUI updates from the JavaFX thread (Platform.runLater()), the JavaFX event queue gets overloaded very quickly. I'm trying to fix this by building a utility class capable of asynchronously updating GUI Properties from outside the JavaFX threads. Rapid consecutive updates should be skipped in order to only display the latest value and thus avoid swarming the JavaFX event queue with Runnables.

I therefore built the following class GUIUpdater to bind Properties (Usually a GUI element such as a Label) to ObservableValues (like a SimpleStringProperty). This class has two InnerClasses:

  • PropertyUpdateris responsible for binding a single Property to a single ObservableValue and updating it.
  • The Updater provides a re-usable Runnable object for Platform.runLater().

The utility class:

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

    }
}

And this is a simple class for testing the GUIUpdater utility class:

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

    }

}

The problem with the code is that sometimes the Label is not updated to the latest value (in this case 999999). It seems to mostly happen right after the Application is started, so starting the app, clicking the "Start" button, closing it and repeating this process should replicate the problem after a few tries. As far as I can see I've added synchronized blocks where needed which is why I don't understand where the problem comes from.

Even though I'm primarily looking for the solution to the described problem all suggestions are very much appreciated (even those not related to the problem)! I added comments in the code as well, so I hope that together with the information above this provides enough detail about the problem and the code.

Thanks in advance!

解决方案

I was able to fix the issue myself. After a few days of adding System.out in various places it turned out the problem was due to concurrency issues with the isUpdating variable. The problem occured when the JavaFX thread was inbetween the while loop and the synchronized block in Updater.run. I solved the problem by making both the Updater.run and GUIUpdater.scheduleUpdate methods synchronized on the same object.

I also made the GUIUpdater into a static-only object as having multiple instances will place Runnables in the JavaFX event queue regardless of the other GUIUpdater instances, clogging up the event queue. All in all, this is the resulting GUIUpdater class:

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

    }
}

And this is a slightly updated version of the tester class (I'm not going into detail on this one as it's totally unimportant):

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

    }

}

这篇关于JavaFX GUI更新器实用程序类中的并发问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆