Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ConcurrentModificationException when updating JavaFX Tree in the background

Tags:

java

javafx-2

My code creates TreeItem<String> in a background task since I have a lot of them and their creation takes a considerable amount of time in which the application freezes. In this example it doesn't make much sense but it illustrates the problem I run into in my actual application. When expanding nodes the program throws a ConcurrentModificationException.

I use jdk1.7.0_17 and JavaFX 2.2.7

Does anyone know how to create a thread-safe Tree or how to circumvent the problem?

Exception

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)
    ...

Code

import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.security.SecureRandom;
import java.util.Random;


public class ConcurrentExample extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        TreeView<String> treeView = new TreeView<String>(createNode("root"));
        HBox hBox = new HBox();
        hBox.getChildren().addAll(treeView);
        Scene scene = new Scene(hBox);
        stage.setScene(scene);
        stage.show();
    }

    Random r = new SecureRandom();

    public TreeItem<String> createNode(final String b) {
        return new TreeItem<String>(b) {
            private boolean isLeaf;
            private boolean isFirstTimeChildren = true;
            private boolean isFirstTimeLeaf = true;

            @Override
            public ObservableList<TreeItem<String>> getChildren() {
                if (isFirstTimeChildren) {
                    isFirstTimeChildren = false;
                    buildChildren(super.getChildren());
                }
                return super.getChildren();
            }

            @Override
            public boolean isLeaf() {
                if (isFirstTimeLeaf) {
                    isFirstTimeLeaf = false;
                    isLeaf = r.nextBoolean() && r.nextBoolean() && r.nextBoolean();
                }
                return isLeaf;
            }

            private void buildChildren(final ObservableList<TreeItem<String>> children) {
                if (!this.isLeaf()) {
                    Task<Integer> task = new Task<Integer>() {
                        @Override
                        protected Integer call() throws Exception {
                            int i;
                            int max = r.nextInt(500);
                            for (i = 0; i <= max; i++) {
                                children.addAll(new TreeItem[]{createNode("#" + r.nextInt())});
                            }
                            return i;
                        }
                    };
                    new Thread(task).start();
                }
            }
        };
    }

}
like image 267
Sebastian Annies Avatar asked Apr 20 '13 21:04

Sebastian Annies


2 Answers

The current answers do not help. The point is that you have to execute the update of the children in the main Thread with the help of Platform.runLater

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.security.SecureRandom;
import java.util.Random;


public class Example extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        TreeView<String> treeView = new TreeView<String>(createNode("root"));
        HBox hBox = new HBox();
        hBox.getChildren().addAll(treeView);
        Scene scene = new Scene(hBox);
        stage.setScene(scene);
        stage.show();
    }

    Random r = new SecureRandom();

    public TreeItem<String> createNode(final String b) {
        return new TreeItem<String>(b) {
            private boolean isLeaf;
            private boolean isFirstTimeChildren = true;
            private boolean isFirstTimeLeaf = true;

            @Override
            public ObservableList<TreeItem<String>> getChildren() {
                if (isFirstTimeChildren) {
                    isFirstTimeChildren = false;
                    buildChildren(super.getChildren());
                }
                return super.getChildren();
            }

            @Override
            public boolean isLeaf() {
                if (isFirstTimeLeaf) {
                    isFirstTimeLeaf = false;
                    isLeaf = r.nextBoolean() && r.nextBoolean() && r.nextBoolean();
                }
                return isLeaf;
            }

            private void buildChildren(final ObservableList<TreeItem<String>> children) {
                if (!this.isLeaf()) {
                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            int i;
                            int max = r.nextInt(500);
                            for (i = 0; i <= max; i++) {
                                children.addAll(new TreeItem[]{createNode("#" + r.nextInt())});
                            }
                        }
                    });
                }
            }
        };
    }
}

This guy here ran into the same problem: http://blog.idrsolutions.com/2012/12/handling-threads-concurrency-in-javafx/

like image 56
Sebastian Annies Avatar answered Nov 15 '22 01:11

Sebastian Annies


You can't directly modify anything that effects active nodes and data related to the scene graph (and that includes the items of a TreeView) from any thread other than the JavaFX application thread.

See the Task documentation for sample tasks (the ones which return an ObservableList or Partial Results) which will help you solve your issue. You need to create your new TreeItems in your Task in a new ObservableList and then, once the Task is finished (and on the JavaFX application thread), set the item list for your tree to the ObservableList returned from the Task.

http://docs.oracle.com/javafx/2/api/javafx/concurrent/Task.html

Here is an updated version of your code which follows some of these principles and does not have any ConcurrentModificationExceptions.

Why shouldn't the addAll(List) call be performed exactly in the moment where the TreeItem calls updateExpandedDescendentCount()?

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.security.SecureRandom;
import java.util.Random;


public class ConcurrentExample extends Application {
  public static void main(String[] args) {
    launch(args);
  }

  @Override
  public void start(Stage stage) throws Exception {
    TreeView<String> treeView = new TreeView<>(createNode("root"));
    HBox hBox = new HBox();
    hBox.getChildren().addAll(treeView);
    Scene scene = new Scene(hBox);
    stage.setScene(scene);
    stage.show();
  }

  Random r = new SecureRandom();

  public TreeItem<String> createNode(final String b) {
    return new TreeItem<String>(b) {
      private boolean isLeaf;
      private boolean isFirstTimeChildren = true;
      private boolean isFirstTimeLeaf = true;

      @Override
      public ObservableList<TreeItem<String>> getChildren() {
        if (isFirstTimeChildren) {
          isFirstTimeChildren = false;
          buildChildren(super.getChildren());
        }
        return super.getChildren();
      }

      @Override
      public boolean isLeaf() {
        if (isFirstTimeLeaf) {
          isFirstTimeLeaf = false;
          isLeaf = r.nextBoolean() && r.nextBoolean() && r.nextBoolean();
        }
        return isLeaf;
      }

      private void buildChildren(final ObservableList<TreeItem<String>> children) {
        final ObservableList<TreeItem<String>> taskChildren = FXCollections.observableArrayList();

        if (!this.isLeaf()) {
          Task<Integer> task = new Task<Integer>() {
            @Override
            protected Integer call() throws Exception {
              int i;
              int max = r.nextInt(500);
              for (i = 0; i <= max; i++) {
                taskChildren.addAll(new TreeItem[]{createNode("#" + r.nextInt())});
              }
              return i;
            }
          };

          task.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
            @Override public void handle(WorkerStateEvent workerStateEvent) {
              children.setAll(taskChildren);
            }
          });
          new Thread(task).start();
        }
      }
    };
  }

}

Update - Description of why the solution works

The solution above cannot receive a ConcurrentModificationException because the ObservableLists involved are never modified concurrently.

  • The taskChildren collections are only modified on the user thread for the task AND
  • Children of tree items actively attached to the scenegraph are only modified on the JavaFX application thread for the task.

This is ensured by the following items:

  1. taskChildren.addAll is invoked in the task's call method.
  2. A task's call method is invoked on the user thread.
  3. children.setAll(taskChildren) is invoked on the JavaFX application thread.
  4. The JavaFX system ensures that the onSucceeded event handler for a task is invoked on the JavaFX application thread.
  5. After task completion, no more children will ever be added to a given taskChildren list and the list is never modified.
  6. For every task undertaken a new taskChildren list is created, so a given taskChildren list is never shared between tasks.
  7. Every time a modification is made to the tree, a new task is created.
  8. Task semantics are such that a given task can only be run once and never restarted.
  9. The children of the TreeItem attached to the active scenegraph are only modified on the JavaFX application thread after the task has successfully completed and stopped processing.

Why shouldn't the addAll(List) call be performed exactly in the moment where the TreeItem calls updateExpandedDescendentCount()?

updateExpandedDescendentCount() is not part of the public TreeItem api - it is an internal implementation method for the TreeView and is irrelevant to solving this problem.


Update Partial Updates

The JavaFX Task documentation has a solution for "A Task Which Returns Partial Results". Using something similar, you should be able to solve the issue that "the application is unusable in the beginning as one has to wait for the 'buildChildren'-thread to finish to see any nodes.". This is because the partial result solution will allow the results to be "streamed" in small batches from the builder task thread back to the FX application thread.

This kind of solution is more complicated in implementation than the one I provided above, but should allow you to have a responsive UI that fits your requirements. As always, when dealing with concurrent situations, extra care will need to be taken to ensure that shared data is not mutated concurrently causing potential race conditions as you experienced in your original post.

like image 30
jewelsea Avatar answered Nov 15 '22 00:11

jewelsea