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?
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)
...
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();
}
}
};
}
}
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/
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.
taskChildren
collections are only modified on the user thread for the task ANDThis is ensured by the following items:
taskChildren.addAll
is invoked in the task's call
method.children.setAll(taskChildren)
is invoked on the JavaFX application thread.onSucceeded
event handler for a task is invoked on the JavaFX application thread.taskChildren
list and the list is never modified. taskChildren
list is created, so a given taskChildren
list is never shared between tasks.Why shouldn't the
addAll(List)
call be performed exactly in the moment where theTreeItem
callsupdateExpandedDescendentCount()
?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With