【问题标题】:JavaFX & Multithreading: IllegalStateException on ObservableList.add()JavaFX 和多线程:ObservableList.add() 上的 IllegalStateException
【发布时间】:2017-11-11 01:47:08
【问题描述】:

我有一个与服务器通信并从服务器获取数据的 JavaFX 应用程序。接收到的数据放入一个 ObservableList 中,并显示在一个 TableView 中。

与服务器的通信在其自己的线程中运行,并且在调用 ObservableList.add 时会触发 IllegalStateException(抱怨不是事件线程/不是 JavaFX 线程)

我发现了以下solution 来解决类似的问题,但我不确定如何采用它,因为在我的情况下,与服务器的通信需要不断保持,因此任务/线程一直运行到通讯终止。

我在这里有一个最小的工作示例,可以触发所述异常并粗略地模拟应用程序的工作方式。

主要:

package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();
    }


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

控制器:

package sample;

import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;

public class Controller {
    public TableView<Integer> timeTable;
    public TableColumn<Integer, String> positionColumn;

    private ObservableList<Integer> testList;

    @FXML
    private void initialize() {
        testList = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
        positionColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().toString()));
        timeTable.setItems(testList);
        Task<Integer> integerTask = new Test(testList);
        Thread testThread = new Thread(integerTask);

        testThread.start();
    }
}

通讯任务:

package sample;

import javafx.collections.ObservableList;
import javafx.concurrent.Task;

public class Test extends Task<Integer> {
    private ObservableList<Integer> testlist;

    Test(ObservableList<Integer> list) {
        testlist = list;
    }

    @Override
    protected Integer call() throws Exception {
        // Emulates the server communication thread. Instead of an endless loop, I used a fixed number of iterations.
        // The real application has an endless while loop for server communication so a Task cannot be used to
        // get the data

        // getDataFromServer()
        // parseData()
        // putDataInList()
        // loop
        Thread.sleep(2000);
        for (int i = 0; i < 500; ++i) {
            testlist.add(i);
        }

        return 0;
    }
}

FXML:

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.GridPane?>
<GridPane fx:controller="sample.Controller"
          xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
    <TableView fx:id="timeTable" editable="true" prefHeight="498.0"
               prefWidth="254.0">
        <columns>
            <TableColumn fx:id="positionColumn" prefWidth="73.0" text="Position"/>
        </columns>
        <columnResizePolicy>
            <TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
        </columnResizePolicy>
    </TableView>
</GridPane>

【问题讨论】:

    标签: java multithreading javafx task


    【解决方案1】:

    即使列表是同步的,对列表的修改仍然会在进行修改的线程上触发事件。在您的情况下,这会导致在非应用程序线程上触发 TableView 更新。

    此外,您不能简单地使用Task.updateValue,请参阅the javadoc 的以下部分(强调我的):

    updateValue 的调用被合并并稍后在 FX 应用程序线程上运行 [...] 并且中间值可能被合并以节省事件通知。

    您需要自己同步。下面的类结合了更新来补偿快速更新,这些更新可能会通过发布许多可运行文件来减慢应用程序线程:

    public abstract class JavaFXWorker<T> implements Runnable {
    
        private final List<T> results = new LinkedList<>();
        private final Object lock = new Object();
        private boolean updateWaiting = false;
    
        protected void publish(T... values) {
            synchronized (lock) {
                for (T v : values) {
                    results.add(v);
                }
    
                // don't trigger additional updates, if last update didn't fetch the results yet
                // this reduces the number of Runables posted on the application thread
                if (!updateWaiting) {
                    updateWaiting = true;
                    Platform.runLater(this::update);
                }
            }
        }
    
        private void update() {
            List<T> chunks;
            synchronized(lock) {
                // copy results to new list and clear results
                chunks = new ArrayList(results);
                results.clear();
                updateWaiting = false;
            }
            // run ui updates
            process(chunks);
        }
    
        protected abstract void process(List<T> chunks);
    
    }
    

    您的代码可以使用上述类重写如下。

    public class Test extends JavaFXWorker<Integer> {
        private final ObservableList<Integer> testlist;
    
        public Test(ObservableList<Integer> list) {
            testlist = list;
        }
    
        @Override
        public void run() {
            // Emulates the server communication thread. Instead of an endless loop, I used a fixed number of iterations.
            // The real application has an endless while loop for server communication so a Task cannot be used to
            // get the data
    
            // getDataFromServer()
            // parseData()
            // putDataInList()
            // loop
            Thread.sleep(2000);
            for (int i = 0; i < 500; ++i) {
                publish(i);
            }
        }
    
        @Override
        protected process(List<Integer> chunks) {
            testlist.addAll(chunks);
        }
    }
    
    testList = FXCollections.observableArrayList();
    
    ...
    
    Thread testThread = new Thread(new Test(testList));
    
    testThread.start();
    

    【讨论】:

    • 感谢您提供非常详细和深思熟虑的答案。这以一种非常优雅的方式解决了我的问题!
    【解决方案2】:

    不要在Task&lt;Integer&gt;call() 方法的实现中直接更新ObservableList&lt;Integer&gt;,而是使用updateValue() 发布新值,如here 所示Task&lt;Canvas&gt;。合适的ChangeListener 可以安全地更新JavaFX Application thread 上的列表,正如@James_D 讨论的here

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-11-16
      • 2016-01-08
      • 1970-01-01
      • 2020-10-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多