JavaFX: ConcurrentModificationException при добавлении объектов TreeItem в TreeView в отдельном потоке

У меня есть следующий код

public void start(Stage primaryStage) {        
    BorderPane border_pane = new BorderPane();

    TreeView tree = addTreeView();                                          //TreeView on the LEFT
    border_pane.setLeft(tree);

    /* more stuff added to the border_pane here... */

    Scene scene = new Scene(border_pane, 900, 700);
    scene.setFill(Color.GHOSTWHITE);

    primaryStage.setTitle("PlugControl v0.1e");
    primaryStage.setScene(scene);
    primaryStage.show();
}

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

Поскольку addTreeView является функцией, которая считывает данные из базы данных SQL и добавляет около 35 TreeItems на основе этих данных. Добавление TreeItems к treeItemRoot выполняется в отдельном потоке. ПРИМЕЧАНИЕ. treeItemRoot объявлен в основном классе и до этого имел значение null.

public TreeView addTreeView() {                                             //Our treeView is positioned on the LEFT
        treeItemRoot = new PlugTreeItem<>("Active Plugs", new ImageView(new Image(getClass().getResourceAsStream("graphics/plugicon.png"))), new Plug());          //Root of the tree, contains a dummy Plug object.
        selectedTreeItem = treeItemRoot;

        treeItemRoot.setExpanded(true);                                         //always expand it

        selectedTreeItem.getPlugItem()
                .getSIHUid().addListener(new ChangeListener<String>() {
                    @Override
                    public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue
                    ) {
                        System.err.println("changed " + oldValue + "->" + newValue);
                    }
                }
                );

        TreeView<String> treeView = new TreeView<>(treeItemRoot);               //Build the tree with our root node.

        final Task task;
        task = new Task<Void>() {
            @Override
            protected Void call() throws Exception {

                //=========== SQL STUFF BEGINS HERE ============================
                Statement sta = null;
                ResultSet result_set = null;
                Connection conn = null;

                try {
                    try {
                        System.err.println("Loading JDBC driver...");
                        Class.forName("com.mysql.jdbc.Driver");
                        System.err.println("Driver loaded!");
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException("Cannot find JDBC driver in the classpath!", e);
                    }

                    System.err.println("Connecting to database...");
                    conn = DriverManager.getConnection("[DB link here]", "[username]", "[password]");           //Username is PlugControl, pw is woof
                    System.err.println("Connected to Database!");

                    sta = conn.createStatement();
                    String sql_query = "SELECT * FROM pwnodes INNER JOIN pwcomports ON pwnodes.NetworkID = pwcomports.NetworkID WHERE pwnodes.connection = 'on' ORDER BY pwnodes.Location";

                    result_set = sta.executeQuery(sql_query);
                    System.err.println("SQL query successfuly executed!");

                    int count = 0;
                    while (result_set.next()) {
                        Plug pl = null;                                         //MARKER: We might need to do switch (result_set.getString("Server")) for SIHU1 and SIHU2.
                        count++;
                        pl = new Plug(result_set.getString("SIHUid"), result_set.getString("sensorID"), result_set.getString("Location"), result_set.getString("Appliance"), result_set.getString("Type"), result_set.getString("connection"), result_set.getString("Server"), result_set.getString("ServerIP"));
                        PlugTreeItem<String> pti = new PlugTreeItem(pl.getSIHUid().getValue() + " " + pl.getLocation() + " " + pl.getAppliance(), new ImageView(new Image(getClass().getResourceAsStream("graphics/smiley.png"))), pl); //icon does not work in children
                        treeItemRoot.getChildren().add(pti);                    //CONCURRENCY ERRORS HERE
                    }
                    System.err.println("ALERT SQL QUERY RESULTS: " + count);

                } catch (SQLException e) //linked try clause @ line 50
                {
                    throw new RuntimeException("Cannot connect the database!", e);
                } finally {   //  Time to wrap things up, by closing all open SQL procs. 
                    try {
                        if (sta != null) {
                            sta.close();
                        }
                        if (result_set != null) {
                            result_set.close();
                        }
                        if (conn != null) {
                            System.err.println("Closing the connection.");
                            conn.close();
                        }
                    } catch (SQLException e) //We might as well ignore this, but just in case.
                    {
                        throw new RuntimeException("Error while closing up statement, result set and connection!", e);
                    }
                }

                //============== SQL STUFF ENDS HERE ===========================
                System.err.println("Finished");
                return null;
            }
        };

        new Thread(task).start();                                               //Run the task!

        treeView.getSelectionModel()
                .selectedItemProperty().addListener(new ChangeListener() {
                    @Override
                    public void changed(ObservableValue observable, Object oldValue, Object newValue
                    ) {
                        selectedTreeItem = (PlugTreeItem<String>) newValue;
                        System.err.println("DEBUG: Selection plug SIHUid: " + selectedTreeItem.getPlugItem().print());           //MARKER: REMOVE
                        updateTextFields();                                             //Update TextAreas.
                        if (!"DUMMY".equals(selectedTreeItem.getPlugItem().getSIHUid().getValue())) {
                            buttonOn.setDisable(false);
                            buttonOff.setDisable(false);
                        } else {
                            buttonOn.setDisable(true);
                            buttonOff.setDisable(true);
                        }
                    }
                }
                );
        return treeView;
    }

После каждых 5-6 прогонов я получаю ConcurrentModificationException, потому что я думаю, что задача Thread() не успевает завершиться до того, как addTreeView вернет TreeView, добавленный в border_pane, и, возможно, он начнет перебирать его, пока к нему все еще добавляются элементы?

Executing C:\Users\74\Documents\NetBeansProjects\PlugControl_v0.5\dist\run1559674105\PlugControl.jar using platform C:\Program Files\Java\jdk1.7.0_25\jre/bin/java
Loading JDBC driver...
Driver loaded!
Connecting to database...
Connected to Database!
SQL query successfuly executed!
java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
    at java.util.ArrayList$Itr.next(ArrayList.java:791)
    at com.sun.javafx.collections.ObservableListWrapper$ObservableListIterator.next(ObservableListWrapper.java:681)
    at javafx.scene.control.TreeItem.updateExpandedDescendentCount(TreeItem.java:788)
    at javafx.scene.control.TreeItem.getExpandedDescendentCount(TreeItem.java:777)
    at javafx.scene.control.TreeView.getExpandedDescendantCount(TreeView.java:864)
    at javafx.scene.control.TreeView.updateTreeItemCount(TreeView.java:873)
    at javafx.scene.control.TreeView.impl_getTreeItemCount(TreeView.java:533)
    at com.sun.javafx.scene.control.skin.TreeViewSkin.getItemCount(TreeViewSkin.java:207)
    at com.sun.javafx.scene.control.skin.TreeViewSkin.updateItemCount(TreeViewSkin.java:220)
    at com.sun.javafx.scene.control.skin.TreeViewSkin.handleControlPropertyChanged(TreeViewSkin.java:135)
    at com.sun.javafx.scene.control.skin.SkinBase$3.changed(SkinBase.java:282)
    at javafx.beans.value.WeakChangeListener.changed(WeakChangeListener.java:107)
    at com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:196)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:100)
    at javafx.beans.property.IntegerPropertyBase.fireValueChangedEvent(IntegerPropertyBase.java:123)
    at javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:130)
    at javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:163)
    at javafx.scene.control.TreeView.setTreeItemCount(TreeView.java:515)
    at javafx.scene.control.TreeView.updateTreeItemCount(TreeView.java:876)
    at javafx.scene.control.TreeView.impl_getTreeItemCount(TreeView.java:533)
    at javafx.scene.control.TreeCell.updateItem(TreeCell.java:391)
    at javafx.scene.control.TreeCell.access$000(TreeCell.java:67)
    at javafx.scene.control.TreeCell$1.invalidated(TreeCell.java:95)
    at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:155)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:100)
    at javafx.beans.property.ReadOnlyIntegerWrapper$ReadOnlyPropertyImpl.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:195)
    at javafx.beans.property.ReadOnlyIntegerWrapper.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:161)
    at javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:130)
    at javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:163)
    at javafx.scene.control.IndexedCell.updateIndex(IndexedCell.java:112)
    at com.sun.javafx.scene.control.skin.VirtualFlow.setCellIndex(VirtualFlow.java:1596)
    at com.sun.javafx.scene.control.skin.VirtualFlow.addLeadingCells(VirtualFlow.java:1049)
    at com.sun.javafx.scene.control.skin.VirtualFlow.layoutChildren(VirtualFlow.java:1005)
    at com.sun.javafx.scene.control.skin.VirtualFlow.setCellCount(VirtualFlow.java:206)
    at com.sun.javafx.scene.control.skin.TreeViewSkin.updateItemCount(TreeViewSkin.java:225)
    at com.sun.javafx.scene.control.skin.TreeViewSkin.handleControlPropertyChanged(TreeViewSkin.java:135)
    at com.sun.javafx.scene.control.skin.SkinBase$3.changed(SkinBase.java:282)
    at javafx.beans.value.WeakChangeListener.changed(WeakChangeListener.java:107)
    at com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:196)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:100)
    at javafx.beans.property.IntegerPropertyBase.fireValueChangedEvent(IntegerPropertyBase.java:123)
    at javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:130)
    at javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:163)
    at javafx.scene.control.TreeView.setTreeItemCount(TreeView.java:515)
    at javafx.scene.control.TreeView.updateTreeItemCount(TreeView.java:876)
    at javafx.scene.control.TreeView.impl_getTreeItemCount(TreeView.java:533)
    at javafx.scene.control.TreeCell.updateItem(TreeCell.java:391)
    at javafx.scene.control.TreeCell.access$000(TreeCell.java:67)
    at javafx.scene.control.TreeCell$1.invalidated(TreeCell.java:95)
    at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:155)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:100)
    at javafx.beans.property.ReadOnlyIntegerWrapper$ReadOnlyPropertyImpl.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:195)
    at javafx.beans.property.ReadOnlyIntegerWrapper.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:161)
    at javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:130)
    at javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:163)
    at javafx.scene.control.IndexedCell.updateIndex(IndexedCell.java:112)
    at com.sun.javafx.scene.control.skin.VirtualFlow.setCellIndex(VirtualFlow.java:1596)
    at com.sun.javafx.scene.control.skin.VirtualFlow.getCell(VirtualFlow.java:1500)
    at com.sun.javafx.scene.control.skin.VirtualFlow.getCellLength(VirtualFlow.java:1523)
    at com.sun.javafx.scene.control.skin.VirtualFlow$3.call(VirtualFlow.java:478)
    at com.sun.javafx.scene.control.skin.VirtualFlow$3.call(VirtualFlow.java:476)
    at com.sun.javafx.scene.control.skin.PositionMapper.computeViewportOffset(PositionMapper.java:143)
    at com.sun.javafx.scene.control.skin.VirtualFlow.layoutChildren(VirtualFlow.java:1001)
    at javafx.scene.Parent.layout(Parent.java:1018)
    at javafx.scene.Parent.layout(Parent.java:1028)
    at javafx.scene.Parent.layout(Parent.java:1028)
    at javafx.scene.Parent.layout(Parent.java:1028)
    at javafx.scene.Scene.layoutDirtyRoots(Scene.java:516)
    at javafx.scene.Scene.doLayoutPass(Scene.java:487)
    at javafx.scene.Scene.access$3900(Scene.java:170)
    at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2203)
    at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:363)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:460)
    at com.sun.javafx.tk.quantum.QuantumToolkit$9.run(QuantumToolkit.java:329)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.access$100(WinApplication.java:29)
    at com.sun.glass.ui.win.WinApplication$3$1.run(WinApplication.java:73)
    at java.lang.Thread.run(Thread.java:724)
ALERT SQL QUERY RESULTS: 35
Closing the connection.
Finished

Любая помощь/совет по решению проблемы, ребята? Исключение не указывает мне на место в моем коде, и пока я работаю над догадками.}

РЕДАКТИРОВАТЬ: Для справки: PlugTreeItem — это всего лишь TreeItem‹>, который также несет с собой Plug, Plug — это мой класс, который содержит несколько строковых значений. Ничего особенного. public class PlugTreeItem<T> extends TreeItem{ \* code *\}


person Dimitris Sfounis    schedule 27.02.2014    source источник
comment
что plug и PlugTreeItem ?   -  person ItachiUchiha    schedule 27.02.2014
comment
@ItachiUchiha, посмотри мой обновленный вопрос. Спасибо за ваш комментарий :)   -  person Dimitris Sfounis    schedule 27.02.2014


Ответы (2)


Я бы посоветовал вам сделать List<Plug> внутри вашего while loop, а не создавать отдельный объект и добавлять его в дерево, потому что all operations on javafx controls должно быть выполнено в javafx thread, а не в потоке задач!

Создайте список вне тела потока

List<Plug> listOfPlugs = new ArrayList<Plug>();

Затем в цикле while вы можете написать

int count = 0;
while (result_set.next()) {
    Plug pl = null;                                        
    count++;
    pl = new Plug(result_set.getString("SIHUid"), 
       result_set.getString("sensorID"), result_set.getString("Location"), 
       result_set.getString("Appliance"), result_set.getString("Type"), 
       result_set.getString("connection"), result_set.getString("Server"),
       result_set.getString("ServerIP"));
    listOfPlugs.add(p1);                  
}

Позже, после запуска потока, вы можете сделать следующий код

new Thread(task).start();
task.setOnSucceeded(new EventHandler<WorkerStateEvent>()
{ 
   @Override 
   public void handle(WorkerStateEvent workerStateEvent) { 
      for(Plug p1 : listOfPlugs)
      {
         PlugTreeItem<String> pti = new PlugTreeItem(pl.getSIHUid().getValue() 
         + " " + pl.getLocation() + " " + pl.getAppliance(),
         new ImageView(new Image(
         getClass().getResourceAsStream("graphics/smiley.png"))), pl); 
         treeItemRoot.getChildren().add(pti);
      }
}
person ItachiUchiha    schedule 27.02.2014
comment
Ваш ответ, хотя и находится на правильном пути, имеет серьезный недостаток. Поток, поскольку он использует SQL-запросы, может легко завершиться ПОСЛЕ того, как цикл for(), приведенный ниже, начался + закончился, помещая дочерние элементы в treeItemRoot. Решение состоит в том, чтобы поместить цикл for в task.setOnSucceeded(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent workerStateEvent) { \* for loop here *\ } });, гарантируя, что он запустится только после завершения задачи. Я нашел это из другого ответа и в сочетании с вашим ответом я построил свое решение: p - person Dimitris Sfounis; 27.02.2014
comment
о да, очень верно! Я пропустил этот момент в спешке. Я отредактирую и добавлю его для будущих ссылок !!! - person ItachiUchiha; 27.02.2014
comment
Поскольку вы отредактировали его, и ответ теперь правильный, я могу продолжить и принять его как ответ. - person Dimitris Sfounis; 27.02.2014

Я не вижу, чтобы вы синхронизировались с потоком приложения javafx, который требуется при переходе из другого

person tomsontom    schedule 27.02.2014