出现错误后 JavaFX 折线图冻结 - 错误?

JavaFX line chart freeze after it got an error - Bug?

我在 Gluon Mobile 中使用 JavaFX。非常好的用于创建移动应用程序的框架。但是我有一个 JavaFX 折线图,它在出现错误后冻结。

Exception in thread "JavaFX Application Thread"
java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
    at java.util.Collections$UnmodifiableCollection.next(Collections.java:1042)
    at javafx.scene.chart.LineChart.layoutPlotChildren(LineChart.java:468)
    at javafx.scene.chart.XYChart.layoutChartChildren(XYChart.java:731)
    at javafx.scene.chart.Chart.layoutChildren(Chart.java:94)
    at javafx.scene.Parent.layout(Parent.java:1087)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Scene.doLayoutPass(Scene.java:552)
    at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2397)
    at com.sun.javafx.tk.Toolkit.lambda$runPulse(Toolkit.java:398)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:397)
    at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:424)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:518)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:498)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:491)
    at com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit(QuantumToolkit.java:319)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
    at com.sun.glass.ui.gtk.GtkApplication.lambda$null(GtkApplication.java:139)
    at java.lang.Thread.run(Thread.java:748)

折线图是这样的。

enter image description here

但过了一会儿,我能看到一个接一个的点慢慢消失,然后就什么都没有了。就像 Java 外汇折线图停止更新一样。我已经发布了一些评论它在哪里中断以及实际发生了什么。

enter image description here

我正在使用折线图进行实时记录。这是 Java 代码。

                    /*
                     * Get the time format in HH:mm:ss
                     */
                    LocalDateTime now = LocalDateTime.now();
                    String time = dtf.format(now); 

                    if (countMeasurements < MEASUREMENTS) {
                        time_output.getData().add(new XYChart.Data<String, Number>(time, output));
                        time_input.getData().add(new XYChart.Data<String, Number>(time, input));
                        countMeasurements++;
                    } else {
                        /*
                         * Now insert
                         */
                        time_output.getData().add(new XYChart.Data<String, Number>(time, output)); // <-- No update after a while
                        time_input.getData().add(new XYChart.Data<String, Number>(time, input)); // <-- No update after a while
                        /*
                         * Delete the first object
                         */
                        time_output.getData().remove(0); // <--- This works
                        time_input.getData().remove(0); // <-- This works
                    }

编辑:

这是我收到错误时的样子。点删除越来越多。你看到那条线消失了吗?

enter image description here

完整代码

package com.gluonapplication.thread;


import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import com.gluonhq.charm.glisten.mvc.View;
import de.re.easymodbus.modbusclient.ModbusClient;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;

public class ModbusConnection extends Thread{

    /*
     * Static
     */
    private static boolean running;
    private static boolean start;

    /*
     * Fields from Scene Builder
     */
    private TextField statusTextField;
    private TextField ipAddressTextField;
    private ComboBox<String> startSignalComboBox;
    private TextField predictHorizonTextField;
    private TextField controlHorizonTextField;
    private TextField sampleTimeTextField;
    private TextField referencePointTextField;
    private TextField portTextField;
    private TextField slopeTextField;
    private TextField offsetTextField;
    private LineChart<String, Number> lineChart_primary;
    private Series<String, Number> time_output;
    private LineChart<String, Number> lineChart_third;
    private Series<String, Number> time_input;

    /*
     * Modbus
     */
    private ModbusClient modbusClient;
    private int MEASUREMENTS = 20;
    DateTimeFormatter dtf;
    int[] writeRegisters = new int[11];
    private int countMeasurements;

    @SuppressWarnings("unchecked")
    public ModbusConnection(View primaryView, View secondaryView, View thirdView) {
        /*
         * We start this thread as default
         */
        start = true; 

        /*
         * Initial modbus start
         */
        writeRegisters[6] = 0; 

        /*
         * For secondaryView
         */
        statusTextField = (TextField) secondaryView.lookup("#statusTextField");
        ipAddressTextField = (TextField) secondaryView.lookup("#ipAddressTextField");
        startSignalComboBox = (ComboBox<String>) secondaryView.lookup("#startSignalComboBox");
        predictHorizonTextField = (TextField) secondaryView.lookup("#predictHorizonTextField");
        controlHorizonTextField = (TextField) secondaryView.lookup("#controlHorizonTextField");
        sampleTimeTextField = (TextField) secondaryView.lookup("#sampleTimeTextField");
        referencePointTextField = (TextField) secondaryView.lookup("#referencePointTextField");
        portTextField = (TextField) secondaryView.lookup("#portTextField");
        slopeTextField = (TextField) secondaryView.lookup("#slopeTextField");
        offsetTextField = (TextField) secondaryView.lookup("#offsetTextField");

        /*
         * For primaryView
         */
        lineChart_primary = (LineChart<String, Number>) primaryView.lookup("#lineChart");

        /*
         * Declare the data object inside the chart
         */
        time_output = new Series<String, Number>();
        time_output.setName("Output");
        lineChart_primary.getData().add(time_output);

        /*
         * For thirdView
         */
        lineChart_third = (LineChart<String, Number>) thirdView.lookup("#lineChart");

        /*
         * Declare the data object inside the chart
         */
        time_input = new Series<String, Number>();
        time_input.setName("Input");
        lineChart_third.getData().add(time_input);


        /*
         * This will prevent so we don't get NullPointerException
         */
        modbusClient = null;

        /*
         * For time
         */
        dtf = DateTimeFormatter.ofPattern("HH:mm:ss");  

        /*
         * Reset 
         */
        countMeasurements = 0;
    }

    @Override
    public void run() {
        while (start) {
            while (running == true || writeRegisters[6] == 1) {

                /*
                 * Connect to Modbus server 
                 */
                if(modbusClient == null) {
                    modbusClient = new ModbusClient(ipAddressTextField.getText(),Integer.parseInt(portTextField.getText()));
                    try {
                        modbusClient.Connect();
                    } catch (Exception e) {
                        statusTextField.setText("Cannot connect");
                    }
                }else if(modbusClient.isConnected() == false){
                    modbusClient = new ModbusClient(ipAddressTextField.getText(),Integer.parseInt(portTextField.getText()));
                    try {
                        modbusClient.Connect();
                    } catch (Exception e) {
                        statusTextField.setText("Cannot connect");
                    }
                }

                /*
                 * Write registers at address 0
                 */
                try {                   

                    /*
                     * What start signal should we use. 
                     */
                    int mode = startSignalComboBox.getSelectionModel().getSelectedIndex();
                    switch (mode) {
                        case 0:
                            writeRegisters[0] = 255; // PWM 100%
                            break;
                        case 1:
                            writeRegisters[0] = 230; // PWM 90%
                            break;
                        case 2:
                            writeRegisters[0] = 204; // PWM 80%
                            break;
                        case 3:
                            writeRegisters[0] = 179; // PWM 70%
                            break;
                        case 4:
                            writeRegisters[0] = 153; // PWM 60%
                            break;
                        case 5:
                            writeRegisters[0] = 128; // PWM 50%
                            break;
                        case 6:
                            writeRegisters[0] = 102; // PWM 40%
                            break;
                        case 7:
                            writeRegisters[0] = 77; // PWM 30%
                            break;
                        case 8:
                            writeRegisters[0] = 51; // PWM 20%
                            break;
                        case 9:
                            writeRegisters[0] = 26; // PWM 10%
                            break;
                        default:
                            writeRegisters[0] = 255; // PWM 100%
                            break;
                    }

                    /*
                     * Get the prediction horizon as int
                     */
                    writeRegisters[1] = Integer.parseInt(predictHorizonTextField.getText());

                    /*
                     * Get the control horizon as int
                     */
                    writeRegisters[2] = Integer.parseInt(controlHorizonTextField.getText());

                    /*
                     * Get the sample time in as int
                     */
                    writeRegisters[3] = Integer.parseInt(sampleTimeTextField.getText()); 

                    /*
                     * Get the reference point in two ints
                     */
                    writeRegisters[4] = (int) Float.parseFloat(referencePointTextField.getText());  
                    writeRegisters[5] = (int) ((Float.parseFloat(referencePointTextField.getText()) - ((float) writeRegisters[4])) * 10000); 

                    /*
                     * Get if the system is running
                     */
                    writeRegisters[6] = running ? 1 : 0;

                    /*
                     * Get the slope
                     */
                    writeRegisters[7] = (int) Float.parseFloat(slopeTextField.getText());
                    writeRegisters[8] = (int) ((Float.parseFloat(slopeTextField.getText()) - ((float) writeRegisters[7])) * 10000); 

                    /*
                     * Get the offset
                     */
                    writeRegisters[9] = (int) Float.parseFloat(offsetTextField.getText());  
                    writeRegisters[10] = (int) ((Float.parseFloat(offsetTextField.getText()) - ((float) writeRegisters[9])) * 10000); 

                    /*
                     * Write 11 elements from address 0
                     */
                    modbusClient.WriteMultipleRegisters(0, writeRegisters);

                } catch (Exception e) {
                    statusTextField.setText("Cannot write");
                } 


                /*
                 * Read 3 registers at the beginning from address 12
                 */
                try {
                    int[] registersRead = modbusClient.ReadHoldingRegisters(12, 3); // two first are output (float) and the last is the input (int)

                    /*
                     * Get the output value 
                     */

                    System.out.println("registersRead[0] = " + registersRead[0] + " registersRead[1] = " + registersRead[1] );
                    double output = ((double) registersRead[0]) + ((double) registersRead[1]) / 10000.0;
                    System.out.println("Output : " + output);

                    /*
                     * Get the input value
                     */
                    int input = registersRead[2];

                    /*
                     * Get the time format in HH:mm:ss
                     */
                    LocalDateTime now = LocalDateTime.now();
                    String time = dtf.format(now); 
                    System.out.println("Time : " + time);

                    if (countMeasurements < MEASUREMENTS) {
                        time_output.getData().add(new XYChart.Data<String, Number>(time, output));
                        time_input.getData().add(new XYChart.Data<String, Number>(time, input));
                        countMeasurements++;
                    } else {
                        /*
                         * Now insert
                         */
                        System.out.println("Add time_output");
                        time_output.getData().add(new XYChart.Data<String, Number>(time, output));
                        System.out.println("Add time_input");
                        time_input.getData().add(new XYChart.Data<String, Number>(time, input));
                        /*
                         * Delete the first object
                         */
                        System.out.println("Delete");
                        time_output.getData().remove(0); 
                        time_input.getData().remove(0);
                    }

                } catch (Exception e) {
                    statusTextField.setText("Cannot read");
                } 


                try {
                    Thread.sleep((long) (1000 * Double.parseDouble(sampleTimeTextField.getText())));
                } catch (Exception e) {
                    statusTextField.setText("Cannot delay");
                }

                statusTextField.setText("Running");

            }

            /*
             * This is because we don't want update so fast
             */
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                statusTextField.setText("Cannot delay");
            }

            /*
             * Disconnect if we have been using modbusClient object before
             */
            if(modbusClient != null) {
                try {
                    if(modbusClient.isConnected()) {
                        modbusClient.Disconnect();
                        statusTextField.setText("Stopped");
                    }
                } catch (Exception e) {
                    statusTextField.setText("Cannot disconnect");
                }
            }
        }
    }

    public static boolean isRunning() {
        return running;
    }

    public static void setRunning(boolean running) {
        ModbusConnection.running = running;
    }

    public static boolean isStart() {
        return start;
    }

    public static void setStart(boolean start) {
        ModbusConnection.start = start;
    }

}

编辑 2: enter image description here

如评论中所述,您正在创建后台任务:

public class ModbusConnection extends Thread {

    @Override
    public void run() {
        while (start) {
            ...
        }
    }
}

虽然这对于您的 背景 任务来说非常好,即您的 modbus 连接和通信,但您永远不应该在其上执行 UI 相关任务。

A Chart 是一个 JavaFX 节点,当您将新数据点添加到其系列之一时,如下所示:

public class ModbusConnection extends Thread {

    @Override
    public void run() {
        while (start) {
            ...
            time_output.getData().add(new XYChart.Data<String, Number>(time, output));
            ...               
        }
    }
}

触发布局传递以呈现相关节点,但这应该只在UI线程(JavaFX 应用程序线程)中完成。

因此,作为初始修复,请修改您的代码以执行以下操作:

public class ModbusConnection extends Thread {

    @Override
    public void run() {
        while (start) {
            ...
            Platform.runLater(() -> 
                time_output.getData().add(new XYChart.Data<String, Number>(time, output)));
            ...               
        }
    }
}

请注意,正如@Slaw 在上面的评论中提到的,Platform::runLater 的 JavaDoc 说:

public static void runLater​(Runnable runnable)

Run the specified Runnable on the JavaFX Application Thread at some unspecified time in the future. This method, which may be called from any thread, will post the Runnable to an event queue and then return immediately to the caller.

所以这看起来正是我们在这种情况下所需要的。

但如果你继续阅读:

NOTE: applications should avoid flooding JavaFX with too many pending Runnables. Otherwise, the application may become unresponsive. Applications are encouraged to batch up multiple operations into fewer runLater calls. Additionally, long-running operations should be done on a background thread where possible, freeing up the JavaFX Application Thread for GUI operations.

因此,在第二步中,您应该尝试对来自后台任务的所有调用进行批处理,例如:

public class ModbusConnection extends Thread {

    @Override
    public void run() {
        while (start) {
            ...
            Platform.runLater(() -> {
                time_output.getData().add(new XYChart.Data<String, Number>(time, output));
                time_input.getData().add(new XYChart.Data<String, Number>(time, input));
                if (time_output.getData().size() > MEASUREMENTS) {
                    time_output.getData().remove(0); 
                    time_input.getData().remove(0);
                }
            });
            ...               
        }
    }
}